Refactor lint config and retry logic
This commit is contained in:
+8
-2
@@ -26,7 +26,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['jest.config.js', '.eslintrc.js'],
|
||||
files: ['jest.config.js', '.eslintrc.js', 'jest.setup.js'],
|
||||
parserOptions: {
|
||||
project: null,
|
||||
},
|
||||
@@ -37,5 +37,11 @@ module.exports = {
|
||||
es2020: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: ['**/__mocks__/**', '**/*.test.ts'],
|
||||
ignorePatterns: [
|
||||
'**/__mocks__/**',
|
||||
'**/*.test.ts',
|
||||
'src/**/*.js',
|
||||
'jest.setup.js',
|
||||
'jest.config.js',
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
// Debug the heading matching
|
||||
const heading = "Algorithm Design";
|
||||
const content = "This file mentions algorithm somewhere in the body text";
|
||||
const query = "algorithm";
|
||||
|
||||
function stemToken(token) {
|
||||
if (token.endsWith('s')) return token.slice(0, -1);
|
||||
if (token.endsWith('ed')) return token.slice(0, -2);
|
||||
if (token.endsWith('ing')) return token.slice(0, -3);
|
||||
return token;
|
||||
}
|
||||
|
||||
function tokenize(text) {
|
||||
const stopWords = new Set(['the', 'a', 'an', 'and', 'or', 'but', 'is', 'are', 'was', 'were', 'in', 'on', 'at', 'to', 'of', 'for', 'with', 'as', 'by', 'it', 'its', 'that', 'this', 'these', 'those']);
|
||||
return text
|
||||
.toLowerCase()
|
||||
.split(/\W+/)
|
||||
.filter((token) => token.length > 1 && !stopWords.has(token));
|
||||
}
|
||||
|
||||
const queryTokens = tokenize(query);
|
||||
const headingTokens = tokenize(heading);
|
||||
const contentTokens = tokenize(content);
|
||||
|
||||
console.log("Query:", query);
|
||||
console.log("Query tokens:", queryTokens);
|
||||
console.log("Heading:", heading);
|
||||
console.log("Heading tokens:", headingTokens);
|
||||
console.log("Content:", content);
|
||||
console.log("Content tokens:", contentTokens);
|
||||
|
||||
const queryStemmed = queryTokens.map(t => stemToken(t));
|
||||
const headingStemmed = headingTokens.map(t => stemToken(t));
|
||||
const contentStemmed = contentTokens.map(t => stemToken(t));
|
||||
|
||||
console.log("Query stemmed:", queryStemmed);
|
||||
console.log("Heading stemmed:", headingStemmed);
|
||||
console.log("Content stemmed:", contentStemmed);
|
||||
|
||||
// Check heading match
|
||||
const headingMatch = headingStemmed.some(h => h.includes(stemToken(queryStemmed[0])));
|
||||
console.log("Heading match:", headingMatch);
|
||||
|
||||
// Check content match
|
||||
const contentMatch = contentStemmed.includes(stemToken(queryStemmed[0]));
|
||||
console.log("Content match:", contentMatch);
|
||||
@@ -1,34 +0,0 @@
|
||||
// Debug heading extraction
|
||||
const file1Content = "# Algorithm Design\n\nThis discusses design patterns";
|
||||
const file2Content = "This file mentions algorithm somewhere in the body text";
|
||||
|
||||
function extractHeadings(content) {
|
||||
const headingMatches = content.match(/^# (.*?)$/gm);
|
||||
if (headingMatches) {
|
||||
return headingMatches.map((h) => h.replace(/^# /, ''));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log("File 1 content:", file1Content);
|
||||
console.log("File 1 headings:", extractHeadings(file1Content));
|
||||
console.log("File 2 content:", file2Content);
|
||||
console.log("File 2 headings:", extractHeadings(file2Content));
|
||||
|
||||
// Check if there's any issue with the regex
|
||||
const allLines1 = file1Content.split('\n');
|
||||
const allLines2 = file2Content.split('\n');
|
||||
|
||||
console.log("File 1 lines:", allLines1);
|
||||
console.log("File 2 lines:", allLines2);
|
||||
|
||||
// Check each line for heading match
|
||||
allLines1.forEach((line, i) => {
|
||||
const match = line.match(/^# (.*?)$/);
|
||||
console.log(`File 1 line ${i}: "${line}" -> heading match: ${!!match}`);
|
||||
});
|
||||
|
||||
allLines2.forEach((line, i) => {
|
||||
const match = line.match(/^# (.*?)$/);
|
||||
console.log(`File 2 line ${i}: "${line}" -> heading match: ${!!match}`);
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
// Debug the scoring logic
|
||||
const file1Content = "# Algorithm Design\n\nThis discusses design patterns";
|
||||
const file2Content = "This file mentions algorithm somewhere in the body text";
|
||||
const query = "algorithm";
|
||||
|
||||
function stemToken(token) {
|
||||
if (token.endsWith('s')) return token.slice(0, -1);
|
||||
if (token.endsWith('ed')) return token.slice(0, -2);
|
||||
if (token.endsWith('ing')) return token.slice(0, -3);
|
||||
return token;
|
||||
}
|
||||
|
||||
function tokenize(text) {
|
||||
const stopWords = new Set(['the', 'a', 'an', 'and', 'or', 'but', 'is', 'are', 'was', 'were', 'in', 'on', 'at', 'to', 'of', 'for', 'with', 'as', 'by', 'it', 'its', 'that', 'this', 'these', 'those']);
|
||||
return text
|
||||
.toLowerCase()
|
||||
.split(/\W+/)
|
||||
.filter((token) => token.length > 1 && !stopWords.has(token));
|
||||
}
|
||||
|
||||
function exactMatch(content, token) {
|
||||
const stemmed = stemToken(token);
|
||||
const contentTokens = tokenize(content);
|
||||
return contentTokens.some((ct) => stemToken(ct) === stemmed);
|
||||
}
|
||||
|
||||
function extractHeadings(content) {
|
||||
const headingMatches = content.match(/^# (.*?)$/gm);
|
||||
if (headingMatches) {
|
||||
return headingMatches.map((h) => h.replace(/^# /, ''));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function extractContentTokens(content) {
|
||||
const allText = content
|
||||
.replace(/^---.*?---/s, '')
|
||||
.replace(/^#.*?$/gm, '')
|
||||
.replace(/```.*?```/gs, '')
|
||||
.replace(/`.*?`/g, '')
|
||||
.replace(/\[.*?\]\(.*?\)/g, '');
|
||||
return tokenize(allText);
|
||||
}
|
||||
|
||||
const queryTokens = tokenize(query);
|
||||
const file1Headings = extractHeadings(file1Content);
|
||||
const file1Tokens = extractContentTokens(file1Content);
|
||||
const file2Headings = extractHeadings(file2Content);
|
||||
const file2Tokens = extractContentTokens(file2Content);
|
||||
|
||||
console.log("Query tokens:", queryTokens);
|
||||
console.log("File 1 headings:", file1Headings);
|
||||
console.log("File 1 content tokens:", file1Tokens);
|
||||
console.log("File 2 headings:", file2Headings);
|
||||
console.log("File 2 content tokens:", file2Tokens);
|
||||
|
||||
// Calculate scores
|
||||
function calculateScore(headings, contentTokens, queryTokens) {
|
||||
let totalScore = 0;
|
||||
|
||||
for (const queryToken of queryTokens) {
|
||||
let tokenScore = 0;
|
||||
const stemmed = stemToken(queryToken);
|
||||
let matched = false;
|
||||
|
||||
// Weight 2: Heading check
|
||||
if (headings.some((heading) => heading.toLowerCase().includes(stemmed))) {
|
||||
tokenScore += 2;
|
||||
matched = true;
|
||||
}
|
||||
|
||||
// Weight 1: Content token check
|
||||
const contentMatch = contentTokens.includes(stemmed);
|
||||
if (contentMatch) {
|
||||
tokenScore += 1;
|
||||
matched = true;
|
||||
}
|
||||
|
||||
if (matched) {
|
||||
totalScore += tokenScore;
|
||||
}
|
||||
}
|
||||
|
||||
return totalScore;
|
||||
}
|
||||
|
||||
const score1 = calculateScore(file1Headings, file1Tokens, queryTokens);
|
||||
const score2 = calculateScore(file2Headings, file2Tokens, queryTokens);
|
||||
|
||||
console.log("File 1 score:", score1);
|
||||
console.log("File 2 score:", score2);
|
||||
console.log("File 1 should be first:", score1 > score2);
|
||||
@@ -1,30 +0,0 @@
|
||||
// Debug the exactMatch function
|
||||
const text = "algorithm";
|
||||
const query = "algorithm";
|
||||
|
||||
function stemToken(token) {
|
||||
if (token.endsWith('s')) return token.slice(0, -1);
|
||||
if (token.endsWith('ed')) return token.slice(0, -2);
|
||||
if (token.endsWith('ing')) return token.slice(0, -3);
|
||||
return token;
|
||||
}
|
||||
|
||||
function tokenize(text) {
|
||||
const stopWords = new Set(['the', 'a', 'an', 'and', 'or', 'but', 'is', 'are', 'was', 'were', 'in', 'on', 'at', 'to', 'of', 'for', 'with', 'as', 'by', 'it', 'its', 'that', 'this', 'these', 'those']);
|
||||
return text
|
||||
.toLowerCase()
|
||||
.split(/\W+/)
|
||||
.filter((token) => token.length > 1 && !stopWords.has(token));
|
||||
}
|
||||
|
||||
function exactMatch(content, token) {
|
||||
const stemmed = stemToken(token);
|
||||
const contentTokens = tokenize(content);
|
||||
return contentTokens.some((ct) => stemToken(ct) === stemmed);
|
||||
}
|
||||
|
||||
console.log("Text:", text);
|
||||
console.log("Query:", query);
|
||||
console.log("Tokenized text:", tokenize(text));
|
||||
console.log("Stemmed query:", stemToken(query));
|
||||
console.log("Exact match result:", exactMatch(text, query));
|
||||
+1
-1
@@ -26,12 +26,12 @@
|
||||
"@typescript-eslint/parser": "^6.19.1",
|
||||
"eslint": "^8.56.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^30.3.0",
|
||||
"prettier": "^3.2.5",
|
||||
"ts-jest": "^29.1.2",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"jest-environment-jsdom": "^30.3.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"obsidian": "^1.4.11"
|
||||
}
|
||||
|
||||
+99
-94
@@ -2,8 +2,6 @@
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ChatView = void 0;
|
||||
const obsidian_1 = require("obsidian");
|
||||
const DEFAULT_VAULT_SEARCH_LIMIT = 3;
|
||||
const MAX_MESSAGE_HISTORY = 50;
|
||||
const MAX_STREAM_CHUNKS = 1000;
|
||||
const ollama_client_1 = require("./ollama-client");
|
||||
const vault_indexer_1 = require("./vault-indexer");
|
||||
@@ -213,7 +211,7 @@ class ChatView extends obsidian_1.ItemView {
|
||||
updateLastMessage(content) {
|
||||
const streamingMessage = this.messages.find((msg) => msg.isStreaming);
|
||||
if (streamingMessage && !this.lastMessageEl) {
|
||||
this.lastMessageEl = this.contentEl.createEl('div', {
|
||||
this.lastMessageEl = (this.chatContainer ?? this.contentEl).createEl('div', {
|
||||
cls: `ollama-message assistant`,
|
||||
});
|
||||
this.lastMessageEl.setAttribute('data-msg-id', streamingMessage.id);
|
||||
@@ -222,6 +220,97 @@ class ChatView extends obsidian_1.ItemView {
|
||||
this.lastMessageEl.textContent = content;
|
||||
}
|
||||
}
|
||||
getTools() {
|
||||
return [
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'create_file',
|
||||
description: 'Create a new file in the vault',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: "Relative path within the vault, e.g. 'Notes/todo.md'",
|
||||
},
|
||||
content: { type: 'string', description: 'Content of the file to create' },
|
||||
},
|
||||
required: ['path', 'content'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
buildMessages(userMessage, context) {
|
||||
const systemContent = context
|
||||
? `You are a helpful assistant.\n\nRelevant vault context:\n${context}`
|
||||
: 'You are a helpful assistant.';
|
||||
const systemMessage = {
|
||||
role: 'system',
|
||||
content: systemContent,
|
||||
};
|
||||
const userMessageWithContext = {
|
||||
role: 'user',
|
||||
content: userMessage,
|
||||
};
|
||||
return [
|
||||
systemMessage,
|
||||
...this.messages.map((m) => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
tool_calls: m.tool_calls,
|
||||
})),
|
||||
userMessageWithContext,
|
||||
];
|
||||
}
|
||||
async processToolCalls(toolCalls, messages, tools, fullResponse, assistantMessageId) {
|
||||
// Validate tool calls before processing
|
||||
const MAX_TOOL_CALLS = 10;
|
||||
if (toolCalls.length > MAX_TOOL_CALLS) {
|
||||
throw new Error(`Too many tool calls (max ${MAX_TOOL_CALLS})`);
|
||||
}
|
||||
// Collect all tool results using allSettled to support partial results
|
||||
const settledResults = await Promise.allSettled(toolCalls.map((call) => this.toolExecutor.handleToolCall(call)));
|
||||
const toolResults = [];
|
||||
for (const result of settledResults) {
|
||||
if (result.status === 'fulfilled') {
|
||||
toolResults.push(result.value);
|
||||
}
|
||||
else {
|
||||
// Use centralized error handler for tool errors
|
||||
error_handler_1.ErrorHandler.handleError(result.reason, 'ChatView.handleUserInput');
|
||||
}
|
||||
}
|
||||
// Only create follow-up when we have tool results
|
||||
if (toolResults.length > 0) {
|
||||
// Create follow-up messages including the assistant's tool calls and results
|
||||
const followUpMessages = [
|
||||
...messages,
|
||||
{ role: 'assistant', content: fullResponse, tool_calls: toolCalls },
|
||||
...toolResults.map((result) => ({
|
||||
role: 'tool',
|
||||
content: JSON.stringify(result),
|
||||
})),
|
||||
];
|
||||
const followUp = await this.ollamaClient.chat(followUpMessages, tools);
|
||||
fullResponse += followUp.content;
|
||||
this.updateLastMessage(fullResponse);
|
||||
// Update the assistant message with the final response immutably
|
||||
this.updateMessageById(assistantMessageId, {
|
||||
content: fullResponse,
|
||||
isStreaming: false,
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Even if no tool results were successful, mark streaming as complete
|
||||
// to prevent the assistant message from disappearing
|
||||
this.updateMessageById(assistantMessageId, {
|
||||
content: fullResponse,
|
||||
isStreaming: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
async handleUserInput(content) {
|
||||
if (!this.sendButton || !this.inputEl)
|
||||
return;
|
||||
@@ -232,53 +321,15 @@ class ChatView extends obsidian_1.ItemView {
|
||||
if (!userMessage)
|
||||
return;
|
||||
// Search vault using user message as query
|
||||
const entries = await this.vaultIndexer.searchVault(userMessage, DEFAULT_VAULT_SEARCH_LIMIT);
|
||||
const entries = await this.vaultIndexer.searchVault(userMessage, this.settings.vaultSearchLimit);
|
||||
let context = entries.map((entry) => `### ${entry.title}\n${entry.content}`).join('\n\n');
|
||||
// Cap context size to prevent prompt bloat with large vaults
|
||||
const MAX_CONTEXT_LENGTH = 4000;
|
||||
if (context.length > MAX_CONTEXT_LENGTH) {
|
||||
context = context.substring(0, MAX_CONTEXT_LENGTH) + '\n\n... (truncated)';
|
||||
}
|
||||
const systemContent = context
|
||||
? `You are a helpful assistant.\n\nRelevant vault context:\n${context}`
|
||||
: 'You are a helpful assistant.';
|
||||
const systemMessage = {
|
||||
role: 'system',
|
||||
content: systemContent,
|
||||
};
|
||||
const userMessageWithContext = {
|
||||
role: 'user',
|
||||
content: userMessage,
|
||||
};
|
||||
const messages = [
|
||||
systemMessage,
|
||||
...this.messages.map((m) => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
tool_calls: m.tool_calls,
|
||||
})),
|
||||
userMessageWithContext,
|
||||
];
|
||||
const tools = [
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'create_file',
|
||||
description: 'Create a new file in the vault',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: "Relative path within the vault, e.g. 'Notes/todo.md'",
|
||||
},
|
||||
content: { type: 'string', description: 'Content of the file to create' },
|
||||
},
|
||||
required: ['path', 'content'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const messages = this.buildMessages(userMessage, context);
|
||||
const tools = this.getTools();
|
||||
const messageId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const userMessageId = messageId;
|
||||
const assistantMessageId = `${messageId}-assistant`;
|
||||
@@ -324,51 +375,7 @@ class ChatView extends obsidian_1.ItemView {
|
||||
});
|
||||
// Process tool calls with proper follow-up context
|
||||
if (toolCalls.length > 0) {
|
||||
// Validate tool calls before processing
|
||||
const MAX_TOOL_CALLS = 10;
|
||||
if (toolCalls.length > MAX_TOOL_CALLS) {
|
||||
throw new Error(`Too many tool calls (max ${MAX_TOOL_CALLS})`);
|
||||
}
|
||||
// Collect all tool results using allSettled to support partial results
|
||||
const settledResults = await Promise.allSettled(toolCalls.map((call) => this.toolExecutor.handleToolCall(call)));
|
||||
const toolResults = [];
|
||||
for (const result of settledResults) {
|
||||
if (result.status === 'fulfilled') {
|
||||
toolResults.push(result.value);
|
||||
}
|
||||
else {
|
||||
// Use centralized error handler for tool errors
|
||||
error_handler_1.ErrorHandler.handleError(result.reason, 'ChatView.handleUserInput');
|
||||
}
|
||||
}
|
||||
// Only create follow-up when we have tool results
|
||||
if (toolResults.length > 0) {
|
||||
// Create follow-up messages including the assistant's tool calls and results
|
||||
const followUpMessages = [
|
||||
...messages,
|
||||
{ role: 'assistant', content: fullResponse, tool_calls: toolCalls },
|
||||
...toolResults.map((result) => ({
|
||||
role: 'tool',
|
||||
content: JSON.stringify(result),
|
||||
})),
|
||||
];
|
||||
const followUp = await this.ollamaClient.chat(followUpMessages, tools);
|
||||
fullResponse += followUp.content;
|
||||
this.updateLastMessage(fullResponse);
|
||||
// Update the assistant message with the final response immutably
|
||||
this.updateMessageById(assistantMessageId, {
|
||||
content: fullResponse,
|
||||
isStreaming: false,
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Even if no tool results were successful, mark streaming as complete
|
||||
// to prevent the assistant message from disappearing
|
||||
this.updateMessageById(assistantMessageId, {
|
||||
content: fullResponse,
|
||||
isStreaming: false,
|
||||
});
|
||||
}
|
||||
await this.processToolCalls(toolCalls, messages, tools, fullResponse, assistantMessageId);
|
||||
}
|
||||
// Update assistant message immutably — only if no tool calls were processed
|
||||
if (toolCalls.length === 0) {
|
||||
@@ -377,8 +384,8 @@ class ChatView extends obsidian_1.ItemView {
|
||||
});
|
||||
}
|
||||
// Limit conversation history to prevent memory issues
|
||||
if (this.messages.length > MAX_MESSAGE_HISTORY) {
|
||||
this.messages = this.messages.slice(-MAX_MESSAGE_HISTORY);
|
||||
if (this.messages.length > this.settings.maxMessageHistory) {
|
||||
this.messages = this.messages.slice(-this.settings.maxMessageHistory);
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
@@ -391,11 +398,9 @@ class ChatView extends obsidian_1.ItemView {
|
||||
// Use centralized error handler
|
||||
error_handler_1.ErrorHandler.handleError(error, 'ChatView.handleUserInput');
|
||||
// Update any streaming messages to non-streaming state to prevent stale messages
|
||||
// This ensures that if an error occurs during streaming, the assistant message
|
||||
// is still visible (with any partial content received) but won't cause issues
|
||||
// in subsequent requests due to stale isStreaming: true flag
|
||||
this.messages = this.messages.map((msg) => msg.isStreaming ? { ...msg, isStreaming: false } : msg);
|
||||
this.cleanupStreamingResources();
|
||||
this.render();
|
||||
}
|
||||
finally {
|
||||
if (this.sendButton) {
|
||||
|
||||
+120
-104
@@ -5,8 +5,6 @@ type KeyboardEvent = globalThis.KeyboardEvent;
|
||||
type HTMLTextAreaElement = globalThis.HTMLTextAreaElement;
|
||||
type HTMLButtonElement = globalThis.HTMLButtonElement;
|
||||
|
||||
const DEFAULT_VAULT_SEARCH_LIMIT = 3;
|
||||
const MAX_MESSAGE_HISTORY = 50;
|
||||
const MAX_STREAM_CHUNKS = 1000;
|
||||
import {
|
||||
PluginSettings,
|
||||
@@ -258,7 +256,7 @@ export class ChatView extends ItemView {
|
||||
private updateLastMessage(content: string) {
|
||||
const streamingMessage = this.messages.find((msg) => msg.isStreaming);
|
||||
if (streamingMessage && !this.lastMessageEl) {
|
||||
this.lastMessageEl = this.contentEl.createEl('div', {
|
||||
this.lastMessageEl = (this.chatContainer ?? this.contentEl).createEl('div', {
|
||||
cls: `ollama-message assistant`,
|
||||
}) as HTMLElement;
|
||||
this.lastMessageEl.setAttribute('data-msg-id', streamingMessage.id);
|
||||
@@ -268,6 +266,115 @@ export class ChatView extends ItemView {
|
||||
}
|
||||
}
|
||||
|
||||
private getTools(): OllamaTool[] {
|
||||
return [
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'create_file',
|
||||
description: 'Create a new file in the vault',
|
||||
parameters: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
path: {
|
||||
type: 'string' as const,
|
||||
description: "Relative path within the vault, e.g. 'Notes/todo.md'",
|
||||
},
|
||||
content: { type: 'string' as const, description: 'Content of the file to create' },
|
||||
},
|
||||
required: ['path', 'content'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private buildMessages(userMessage: string, context: string): OllamaMessage[] {
|
||||
const systemContent = context
|
||||
? `You are a helpful assistant.\n\nRelevant vault context:\n${context}`
|
||||
: 'You are a helpful assistant.';
|
||||
const systemMessage: OllamaMessage = {
|
||||
role: 'system',
|
||||
content: systemContent,
|
||||
};
|
||||
const userMessageWithContext: OllamaMessage = {
|
||||
role: 'user',
|
||||
content: userMessage,
|
||||
};
|
||||
|
||||
return [
|
||||
systemMessage,
|
||||
...this.messages.map(
|
||||
(m) =>
|
||||
({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
tool_calls: m.tool_calls,
|
||||
}) as OllamaMessage
|
||||
),
|
||||
userMessageWithContext,
|
||||
];
|
||||
}
|
||||
|
||||
private async processToolCalls(
|
||||
toolCalls: ToolCall[],
|
||||
messages: OllamaMessage[],
|
||||
tools: OllamaTool[],
|
||||
fullResponse: string,
|
||||
assistantMessageId: string
|
||||
): Promise<void> {
|
||||
// Validate tool calls before processing
|
||||
const MAX_TOOL_CALLS = 10;
|
||||
if (toolCalls.length > MAX_TOOL_CALLS) {
|
||||
throw new Error(`Too many tool calls (max ${MAX_TOOL_CALLS})`);
|
||||
}
|
||||
|
||||
// Collect all tool results using allSettled to support partial results
|
||||
const settledResults = await Promise.allSettled(
|
||||
toolCalls.map((call) => this.toolExecutor.handleToolCall(call))
|
||||
);
|
||||
|
||||
const toolResults: ToolResult[] = [];
|
||||
for (const result of settledResults) {
|
||||
if (result.status === 'fulfilled') {
|
||||
toolResults.push(result.value);
|
||||
} else {
|
||||
// Use centralized error handler for tool errors
|
||||
ErrorHandler.handleError(result.reason, 'ChatView.handleUserInput');
|
||||
}
|
||||
}
|
||||
|
||||
// Only create follow-up when we have tool results
|
||||
if (toolResults.length > 0) {
|
||||
// Create follow-up messages including the assistant's tool calls and results
|
||||
const followUpMessages: OllamaMessage[] = [
|
||||
...messages,
|
||||
{ role: 'assistant' as const, content: fullResponse, tool_calls: toolCalls },
|
||||
...toolResults.map((result) => ({
|
||||
role: 'tool' as const,
|
||||
content: JSON.stringify(result),
|
||||
})),
|
||||
];
|
||||
|
||||
const followUp = await this.ollamaClient.chat(followUpMessages, tools);
|
||||
fullResponse += followUp.content;
|
||||
this.updateLastMessage(fullResponse);
|
||||
|
||||
// Update the assistant message with the final response immutably
|
||||
this.updateMessageById(assistantMessageId, {
|
||||
content: fullResponse,
|
||||
isStreaming: false,
|
||||
});
|
||||
} else {
|
||||
// Even if no tool results were successful, mark streaming as complete
|
||||
// to prevent the assistant message from disappearing
|
||||
this.updateMessageById(assistantMessageId, {
|
||||
content: fullResponse,
|
||||
isStreaming: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handleUserInput(content: string) {
|
||||
if (!this.sendButton || !this.inputEl) return;
|
||||
this.sendButton.disabled = true;
|
||||
@@ -278,7 +385,10 @@ export class ChatView extends ItemView {
|
||||
if (!userMessage) return;
|
||||
|
||||
// Search vault using user message as query
|
||||
const entries = await this.vaultIndexer.searchVault(userMessage, DEFAULT_VAULT_SEARCH_LIMIT);
|
||||
const entries = await this.vaultIndexer.searchVault(
|
||||
userMessage,
|
||||
this.settings.vaultSearchLimit
|
||||
);
|
||||
let context = entries.map((entry) => `### ${entry.title}\n${entry.content}`).join('\n\n');
|
||||
|
||||
// Cap context size to prevent prompt bloat with large vaults
|
||||
@@ -287,51 +397,8 @@ export class ChatView extends ItemView {
|
||||
context = context.substring(0, MAX_CONTEXT_LENGTH) + '\n\n... (truncated)';
|
||||
}
|
||||
|
||||
const systemContent = context
|
||||
? `You are a helpful assistant.\n\nRelevant vault context:\n${context}`
|
||||
: 'You are a helpful assistant.';
|
||||
const systemMessage: OllamaMessage = {
|
||||
role: 'system',
|
||||
content: systemContent,
|
||||
};
|
||||
const userMessageWithContext: OllamaMessage = {
|
||||
role: 'user',
|
||||
content: userMessage,
|
||||
};
|
||||
|
||||
const messages: OllamaMessage[] = [
|
||||
systemMessage,
|
||||
...this.messages.map(
|
||||
(m) =>
|
||||
({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
tool_calls: m.tool_calls,
|
||||
}) as OllamaMessage
|
||||
),
|
||||
userMessageWithContext,
|
||||
];
|
||||
|
||||
const tools: OllamaTool[] = [
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'create_file',
|
||||
description: 'Create a new file in the vault',
|
||||
parameters: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
path: {
|
||||
type: 'string' as const,
|
||||
description: "Relative path within the vault, e.g. 'Notes/todo.md'",
|
||||
},
|
||||
content: { type: 'string' as const, description: 'Content of the file to create' },
|
||||
},
|
||||
required: ['path', 'content'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const messages = this.buildMessages(userMessage, context);
|
||||
const tools = this.getTools();
|
||||
|
||||
const messageId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
@@ -389,56 +456,7 @@ export class ChatView extends ItemView {
|
||||
|
||||
// Process tool calls with proper follow-up context
|
||||
if (toolCalls.length > 0) {
|
||||
// Validate tool calls before processing
|
||||
const MAX_TOOL_CALLS = 10;
|
||||
if (toolCalls.length > MAX_TOOL_CALLS) {
|
||||
throw new Error(`Too many tool calls (max ${MAX_TOOL_CALLS})`);
|
||||
}
|
||||
|
||||
// Collect all tool results using allSettled to support partial results
|
||||
const settledResults = await Promise.allSettled(
|
||||
toolCalls.map((call) => this.toolExecutor.handleToolCall(call))
|
||||
);
|
||||
|
||||
const toolResults: ToolResult[] = [];
|
||||
for (const result of settledResults) {
|
||||
if (result.status === 'fulfilled') {
|
||||
toolResults.push(result.value);
|
||||
} else {
|
||||
// Use centralized error handler for tool errors
|
||||
ErrorHandler.handleError(result.reason, 'ChatView.handleUserInput');
|
||||
}
|
||||
}
|
||||
|
||||
// Only create follow-up when we have tool results
|
||||
if (toolResults.length > 0) {
|
||||
// Create follow-up messages including the assistant's tool calls and results
|
||||
const followUpMessages: OllamaMessage[] = [
|
||||
...messages,
|
||||
{ role: 'assistant' as const, content: fullResponse, tool_calls: toolCalls },
|
||||
...toolResults.map((result) => ({
|
||||
role: 'tool' as const,
|
||||
content: JSON.stringify(result),
|
||||
})),
|
||||
];
|
||||
|
||||
const followUp = await this.ollamaClient.chat(followUpMessages, tools);
|
||||
fullResponse += followUp.content;
|
||||
this.updateLastMessage(fullResponse);
|
||||
|
||||
// Update the assistant message with the final response immutably
|
||||
this.updateMessageById(assistantMessageId, {
|
||||
content: fullResponse,
|
||||
isStreaming: false,
|
||||
});
|
||||
} else {
|
||||
// Even if no tool results were successful, mark streaming as complete
|
||||
// to prevent the assistant message from disappearing
|
||||
this.updateMessageById(assistantMessageId, {
|
||||
content: fullResponse,
|
||||
isStreaming: false,
|
||||
});
|
||||
}
|
||||
await this.processToolCalls(toolCalls, messages, tools, fullResponse, assistantMessageId);
|
||||
}
|
||||
|
||||
// Update assistant message immutably — only if no tool calls were processed
|
||||
@@ -449,8 +467,8 @@ export class ChatView extends ItemView {
|
||||
}
|
||||
|
||||
// Limit conversation history to prevent memory issues
|
||||
if (this.messages.length > MAX_MESSAGE_HISTORY) {
|
||||
this.messages = this.messages.slice(-MAX_MESSAGE_HISTORY);
|
||||
if (this.messages.length > this.settings.maxMessageHistory) {
|
||||
this.messages = this.messages.slice(-this.settings.maxMessageHistory);
|
||||
}
|
||||
this.render();
|
||||
} finally {
|
||||
@@ -461,13 +479,11 @@ export class ChatView extends ItemView {
|
||||
// Use centralized error handler
|
||||
ErrorHandler.handleError(error, 'ChatView.handleUserInput');
|
||||
// Update any streaming messages to non-streaming state to prevent stale messages
|
||||
// This ensures that if an error occurs during streaming, the assistant message
|
||||
// is still visible (with any partial content received) but won't cause issues
|
||||
// in subsequent requests due to stale isStreaming: true flag
|
||||
this.messages = this.messages.map((msg) =>
|
||||
msg.isStreaming ? { ...msg, isStreaming: false } : msg
|
||||
);
|
||||
this.cleanupStreamingResources();
|
||||
this.render();
|
||||
} finally {
|
||||
if (this.sendButton) {
|
||||
this.sendButton.disabled = false;
|
||||
|
||||
+54
-52
@@ -50,32 +50,30 @@ class OllamaClient {
|
||||
if (response.status >= 500 && attempt < this.maxRetries) {
|
||||
const retryDelay = Math.pow(2, attempt) * 100;
|
||||
utils_1.Logger.warn(`Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`, 'ollama-client');
|
||||
if (attempt < this.maxRetries - 1) {
|
||||
const retryTimeout = new Promise((resolve) => setTimeout(resolve, retryDelay));
|
||||
const abortListener = () => {
|
||||
utils_1.Logger.info('Retry aborted by user', 'ollama-client');
|
||||
};
|
||||
const signal = controller.signal;
|
||||
if (signal) {
|
||||
signal.addEventListener('abort', abortListener);
|
||||
try {
|
||||
await Promise.race([
|
||||
retryTimeout,
|
||||
new Promise((resolve) => {
|
||||
signal.addEventListener('abort', () => resolve(), {
|
||||
once: true,
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
finally {
|
||||
signal.removeEventListener('abort', abortListener);
|
||||
}
|
||||
const retryTimeout = new Promise((resolve) => setTimeout(resolve, retryDelay));
|
||||
const abortListener = () => {
|
||||
utils_1.Logger.info('Retry aborted by user', 'ollama-client');
|
||||
};
|
||||
const signal = controller.signal;
|
||||
if (signal) {
|
||||
signal.addEventListener('abort', abortListener);
|
||||
try {
|
||||
await Promise.race([
|
||||
retryTimeout,
|
||||
new Promise((resolve) => {
|
||||
signal.addEventListener('abort', () => resolve(), {
|
||||
once: true,
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
else {
|
||||
await retryTimeout;
|
||||
finally {
|
||||
signal.removeEventListener('abort', abortListener);
|
||||
}
|
||||
}
|
||||
else {
|
||||
await retryTimeout;
|
||||
}
|
||||
yield* this.streamChatWithRetry(messages, tools, attempt + 1);
|
||||
return;
|
||||
}
|
||||
@@ -148,11 +146,15 @@ class OllamaClient {
|
||||
}
|
||||
}
|
||||
finally {
|
||||
// Abort the local controller
|
||||
controller.abort();
|
||||
// Abort the local controller to release underlying fetch resources if not already aborted
|
||||
if (!controller.signal.aborted) {
|
||||
controller.abort();
|
||||
}
|
||||
// Clean up the reference only if this is still the current stream
|
||||
if (this.currentStreamController === controller) {
|
||||
this.currentStreamController = null;
|
||||
}
|
||||
}
|
||||
// Clean up the reference
|
||||
this.currentStreamController = null;
|
||||
}
|
||||
async chat(messages, tools = []) {
|
||||
return this.chatWithRetry(messages, tools, 0);
|
||||
@@ -178,32 +180,30 @@ class OllamaClient {
|
||||
if (response.status >= 500 && attempt < this.maxRetries) {
|
||||
const retryDelay = Math.pow(2, attempt) * 100;
|
||||
utils_1.Logger.warn(`Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`, 'ollama-client');
|
||||
if (attempt < this.maxRetries - 1) {
|
||||
const retryTimeout = new Promise((resolve) => setTimeout(resolve, retryDelay));
|
||||
const abortListener = () => {
|
||||
utils_1.Logger.info('Retry aborted by user', 'ollama-client');
|
||||
};
|
||||
const signal = controller.signal;
|
||||
if (signal) {
|
||||
signal.addEventListener('abort', abortListener);
|
||||
try {
|
||||
await Promise.race([
|
||||
retryTimeout,
|
||||
new Promise((resolve) => {
|
||||
signal.addEventListener('abort', () => resolve(), {
|
||||
once: true,
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
finally {
|
||||
signal.removeEventListener('abort', abortListener);
|
||||
}
|
||||
const retryTimeout = new Promise((resolve) => setTimeout(resolve, retryDelay));
|
||||
const abortListener = () => {
|
||||
utils_1.Logger.info('Retry aborted by user', 'ollama-client');
|
||||
};
|
||||
const signal = controller.signal;
|
||||
if (signal) {
|
||||
signal.addEventListener('abort', abortListener);
|
||||
try {
|
||||
await Promise.race([
|
||||
retryTimeout,
|
||||
new Promise((resolve) => {
|
||||
signal.addEventListener('abort', () => resolve(), {
|
||||
once: true,
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
else {
|
||||
await retryTimeout;
|
||||
finally {
|
||||
signal.removeEventListener('abort', abortListener);
|
||||
}
|
||||
}
|
||||
else {
|
||||
await retryTimeout;
|
||||
}
|
||||
return this.chatWithRetry(messages, tools, attempt + 1);
|
||||
}
|
||||
throw new types_1.ApiError(`Ollama API error: ${response.status}`, response.status);
|
||||
@@ -212,8 +212,10 @@ class OllamaClient {
|
||||
return (this.toOllamaMessage(data.message) ?? { role: 'assistant', content: '', tool_calls: [] });
|
||||
}
|
||||
finally {
|
||||
// Abort the local controller
|
||||
controller.abort();
|
||||
// Abort the local controller to release underlying fetch resources if not already aborted
|
||||
if (!controller.signal.aborted) {
|
||||
controller.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
throwIfOllamaError(parsed) {
|
||||
|
||||
+48
-48
@@ -77,29 +77,27 @@ export class OllamaClient {
|
||||
`Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`,
|
||||
'ollama-client'
|
||||
);
|
||||
if (attempt < this.maxRetries - 1) {
|
||||
const retryTimeout = new Promise((resolve) => setTimeout(resolve, retryDelay));
|
||||
const abortListener = () => {
|
||||
Logger.info('Retry aborted by user', 'ollama-client');
|
||||
};
|
||||
const signal = controller.signal;
|
||||
if (signal) {
|
||||
signal.addEventListener('abort', abortListener);
|
||||
try {
|
||||
await Promise.race([
|
||||
retryTimeout,
|
||||
new Promise<void>((resolve) => {
|
||||
signal.addEventListener('abort', () => resolve(), {
|
||||
once: true,
|
||||
});
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
signal.removeEventListener('abort', abortListener);
|
||||
}
|
||||
} else {
|
||||
await retryTimeout;
|
||||
const retryTimeout = new Promise((resolve) => setTimeout(resolve, retryDelay));
|
||||
const abortListener = () => {
|
||||
Logger.info('Retry aborted by user', 'ollama-client');
|
||||
};
|
||||
const signal = controller.signal;
|
||||
if (signal) {
|
||||
signal.addEventListener('abort', abortListener);
|
||||
try {
|
||||
await Promise.race([
|
||||
retryTimeout,
|
||||
new Promise<void>((resolve) => {
|
||||
signal.addEventListener('abort', () => resolve(), {
|
||||
once: true,
|
||||
});
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
signal.removeEventListener('abort', abortListener);
|
||||
}
|
||||
} else {
|
||||
await retryTimeout;
|
||||
}
|
||||
yield* this.streamChatWithRetry(messages, tools, attempt + 1);
|
||||
return;
|
||||
@@ -187,8 +185,10 @@ export class OllamaClient {
|
||||
reader.releaseLock();
|
||||
}
|
||||
} finally {
|
||||
// Abort the local controller
|
||||
controller.abort();
|
||||
// Abort the local controller to release underlying fetch resources if not already aborted
|
||||
if (!controller.signal.aborted) {
|
||||
controller.abort();
|
||||
}
|
||||
// Clean up the reference only if this is still the current stream
|
||||
if (this.currentStreamController === controller) {
|
||||
this.currentStreamController = null;
|
||||
@@ -229,29 +229,27 @@ export class OllamaClient {
|
||||
`Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`,
|
||||
'ollama-client'
|
||||
);
|
||||
if (attempt < this.maxRetries - 1) {
|
||||
const retryTimeout = new Promise((resolve) => setTimeout(resolve, retryDelay));
|
||||
const abortListener = () => {
|
||||
Logger.info('Retry aborted by user', 'ollama-client');
|
||||
};
|
||||
const signal = controller.signal;
|
||||
if (signal) {
|
||||
signal.addEventListener('abort', abortListener);
|
||||
try {
|
||||
await Promise.race([
|
||||
retryTimeout,
|
||||
new Promise<void>((resolve) => {
|
||||
signal.addEventListener('abort', () => resolve(), {
|
||||
once: true,
|
||||
});
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
signal.removeEventListener('abort', abortListener);
|
||||
}
|
||||
} else {
|
||||
await retryTimeout;
|
||||
const retryTimeout = new Promise((resolve) => setTimeout(resolve, retryDelay));
|
||||
const abortListener = () => {
|
||||
Logger.info('Retry aborted by user', 'ollama-client');
|
||||
};
|
||||
const signal = controller.signal;
|
||||
if (signal) {
|
||||
signal.addEventListener('abort', abortListener);
|
||||
try {
|
||||
await Promise.race([
|
||||
retryTimeout,
|
||||
new Promise<void>((resolve) => {
|
||||
signal.addEventListener('abort', () => resolve(), {
|
||||
once: true,
|
||||
});
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
signal.removeEventListener('abort', abortListener);
|
||||
}
|
||||
} else {
|
||||
await retryTimeout;
|
||||
}
|
||||
return this.chatWithRetry(messages, tools, attempt + 1);
|
||||
}
|
||||
@@ -263,8 +261,10 @@ export class OllamaClient {
|
||||
this.toOllamaMessage(data.message) ?? { role: 'assistant', content: '', tool_calls: [] }
|
||||
);
|
||||
} finally {
|
||||
// Abort the local controller
|
||||
controller.abort();
|
||||
// Abort the local controller to release underlying fetch resources if not already aborted
|
||||
if (!controller.signal.aborted) {
|
||||
controller.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -147,8 +147,8 @@ function safeParseJson(jsonString) {
|
||||
if (dangerousKeys.some((key) => Object.keys(obj).includes(key))) {
|
||||
return true;
|
||||
}
|
||||
// Recursively check nested objects
|
||||
for (const key in obj) {
|
||||
// Recursively check nested objects (own properties only)
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (checkDangerousPatterns(obj[key])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
+2
-2
@@ -166,8 +166,8 @@ export function safeParseJson(jsonString: string): unknown {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Recursively check nested objects
|
||||
for (const key in obj as Record<string, unknown>) {
|
||||
// Recursively check nested objects (own properties only)
|
||||
for (const key of Object.keys(obj as Record<string, unknown>)) {
|
||||
if (checkDangerousPatterns((obj as Record<string, unknown>)[key])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
+28
-62
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
// src/vault-indexer.ts
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.CancellationToken = exports.InMemoryCache = exports.VaultIndexer = void 0;
|
||||
exports.InMemoryCache = exports.VaultIndexer = void 0;
|
||||
exports.createVaultIndexerWithCache = createVaultIndexerWithCache;
|
||||
const utils_1 = require("./utils");
|
||||
class InMemoryCache {
|
||||
@@ -21,18 +21,6 @@ class InMemoryCache {
|
||||
}
|
||||
}
|
||||
exports.InMemoryCache = InMemoryCache;
|
||||
class CancellationToken {
|
||||
constructor() {
|
||||
this.cancelled = false;
|
||||
}
|
||||
cancel() {
|
||||
this.cancelled = true;
|
||||
}
|
||||
get isCancelled() {
|
||||
return this.cancelled;
|
||||
}
|
||||
}
|
||||
exports.CancellationToken = CancellationToken;
|
||||
class VaultIndexer {
|
||||
constructor(vault, cache) {
|
||||
this.vault = null;
|
||||
@@ -70,10 +58,7 @@ class VaultIndexer {
|
||||
const vault = this.vault;
|
||||
const allFiles = vault.getMarkdownFiles();
|
||||
const results = await this.processFilesInBatches(vault, allFiles, queryTokens);
|
||||
const filteredResults = results
|
||||
.filter((result) => result !== null)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, limit);
|
||||
const filteredResults = results.sort((a, b) => b.score - a.score).slice(0, limit);
|
||||
if (this.cache) {
|
||||
try {
|
||||
await this.cache.put(cacheKey, JSON.stringify(filteredResults));
|
||||
@@ -88,54 +73,35 @@ class VaultIndexer {
|
||||
const batchSize = 10;
|
||||
const results = [];
|
||||
const seenPaths = new Set();
|
||||
const cancellationToken = new CancellationToken();
|
||||
// Removed unused processedCount variable
|
||||
// Set up a check for cancellation every 100 files
|
||||
const checkInterval = setInterval(() => {
|
||||
if (cancellationToken.isCancelled) {
|
||||
clearInterval(checkInterval);
|
||||
}
|
||||
}, 100);
|
||||
try {
|
||||
for (let i = 0; i < files.length; i += batchSize) {
|
||||
if (cancellationToken.isCancelled) {
|
||||
break;
|
||||
}
|
||||
const batch = files.slice(i, i + batchSize);
|
||||
const batchResults = await Promise.all(batch.map(async (file) => {
|
||||
try {
|
||||
const content = await vault.read(file);
|
||||
const tokenized = this.tokenizeContent(content);
|
||||
const scoreResult = this.calculateWeightedScore(tokenized, queryTokens, file);
|
||||
if (scoreResult.score > 0) {
|
||||
const entry = {
|
||||
path: file.path,
|
||||
title: file.basename.replace(/\.md$/, ''),
|
||||
content: content.substring(0, 500),
|
||||
score: scoreResult.score,
|
||||
};
|
||||
if (!seenPaths.has(entry.path)) {
|
||||
seenPaths.add(entry.path);
|
||||
return entry;
|
||||
}
|
||||
return null;
|
||||
for (let i = 0; i < files.length; i += batchSize) {
|
||||
const batch = files.slice(i, i + batchSize);
|
||||
const batchResults = await Promise.all(batch.map(async (file) => {
|
||||
try {
|
||||
const content = await vault.read(file);
|
||||
const tokenized = this.tokenizeContent(content);
|
||||
const scoreResult = this.calculateWeightedScore(tokenized, queryTokens, file);
|
||||
if (scoreResult.score > 0) {
|
||||
const entry = {
|
||||
path: file.path,
|
||||
title: file.basename.replace(/\.md$/, ''),
|
||||
content: content.substring(0, 500),
|
||||
score: scoreResult.score,
|
||||
};
|
||||
if (!seenPaths.has(entry.path)) {
|
||||
seenPaths.add(entry.path);
|
||||
return entry;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
catch (error) {
|
||||
utils_1.Logger.warn(`Failed to process ${file.path}: ${error instanceof Error ? error.message : String(error)}`, 'vault-indexer');
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
const validResults = batchResults.filter((result) => result !== null);
|
||||
results.push(...validResults);
|
||||
// Continue processing all files to ensure we don't miss higher-scoring results
|
||||
// even if we've already found some matches
|
||||
// Removed processedCount increment
|
||||
}
|
||||
}
|
||||
finally {
|
||||
clearInterval(checkInterval);
|
||||
return null;
|
||||
}
|
||||
catch (error) {
|
||||
utils_1.Logger.warn(`Failed to process ${file.path}: ${error instanceof Error ? error.message : String(error)}`, 'vault-indexer');
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
const validResults = batchResults.filter((result) => result !== null);
|
||||
results.push(...validResults);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
+35
-72
@@ -31,18 +31,6 @@ class InMemoryCache implements Cache {
|
||||
}
|
||||
}
|
||||
|
||||
class CancellationToken {
|
||||
private cancelled = false;
|
||||
|
||||
cancel(): void {
|
||||
this.cancelled = true;
|
||||
}
|
||||
|
||||
get isCancelled(): boolean {
|
||||
return this.cancelled;
|
||||
}
|
||||
}
|
||||
|
||||
interface Frontmatter {
|
||||
title?: string;
|
||||
tags?: string;
|
||||
@@ -112,10 +100,7 @@ class VaultIndexer {
|
||||
const allFiles = vault.getMarkdownFiles();
|
||||
const results = await this.processFilesInBatches(vault, allFiles, queryTokens);
|
||||
|
||||
const filteredResults = results
|
||||
.filter((result): result is NonNullable<typeof result> => result !== null)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, limit);
|
||||
const filteredResults = results.sort((a, b) => b.score - a.score).slice(0, limit);
|
||||
|
||||
if (this.cache) {
|
||||
try {
|
||||
@@ -135,69 +120,47 @@ class VaultIndexer {
|
||||
vault: VaultLike,
|
||||
files: VaultFile[],
|
||||
queryTokens: string[]
|
||||
): Promise<Array<VaultIndexEntry | null>> {
|
||||
): Promise<VaultIndexEntry[]> {
|
||||
const batchSize = 10;
|
||||
const results: VaultIndexEntry[] = [];
|
||||
const seenPaths = new Set<string>();
|
||||
const cancellationToken = new CancellationToken();
|
||||
// Removed unused processedCount variable
|
||||
|
||||
// Set up a check for cancellation every 100 files
|
||||
const checkInterval = setInterval(() => {
|
||||
if (cancellationToken.isCancelled) {
|
||||
clearInterval(checkInterval);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
try {
|
||||
for (let i = 0; i < files.length; i += batchSize) {
|
||||
if (cancellationToken.isCancelled) {
|
||||
break;
|
||||
}
|
||||
|
||||
const batch = files.slice(i, i + batchSize);
|
||||
const batchResults = await Promise.all(
|
||||
batch.map(async (file) => {
|
||||
try {
|
||||
const content = await vault.read(file);
|
||||
const tokenized = this.tokenizeContent(content);
|
||||
const scoreResult = this.calculateWeightedScore(tokenized, queryTokens, file);
|
||||
if (scoreResult.score > 0) {
|
||||
const entry: VaultIndexEntry = {
|
||||
path: file.path,
|
||||
title: file.basename.replace(/\.md$/, ''),
|
||||
content: content.substring(0, 500),
|
||||
score: scoreResult.score,
|
||||
};
|
||||
if (!seenPaths.has(entry.path)) {
|
||||
seenPaths.add(entry.path);
|
||||
return entry;
|
||||
}
|
||||
return null;
|
||||
for (let i = 0; i < files.length; i += batchSize) {
|
||||
const batch = files.slice(i, i + batchSize);
|
||||
const batchResults = await Promise.all(
|
||||
batch.map(async (file) => {
|
||||
try {
|
||||
const content = await vault.read(file);
|
||||
const tokenized = this.tokenizeContent(content);
|
||||
const scoreResult = this.calculateWeightedScore(tokenized, queryTokens, file);
|
||||
if (scoreResult.score > 0) {
|
||||
const entry: VaultIndexEntry = {
|
||||
path: file.path,
|
||||
title: file.basename.replace(/\.md$/, ''),
|
||||
content: content.substring(0, 500),
|
||||
score: scoreResult.score,
|
||||
};
|
||||
if (!seenPaths.has(entry.path)) {
|
||||
seenPaths.add(entry.path);
|
||||
return entry;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
Logger.warn(
|
||||
`Failed to process ${file.path}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
'vault-indexer'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
return null;
|
||||
} catch (error) {
|
||||
Logger.warn(
|
||||
`Failed to process ${file.path}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
'vault-indexer'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const validResults = batchResults.filter(
|
||||
(result): result is NonNullable<typeof result> => result !== null
|
||||
);
|
||||
results.push(...validResults);
|
||||
|
||||
// Continue processing all files to ensure we don't miss higher-scoring results
|
||||
// even if we've already found some matches
|
||||
|
||||
// Removed processedCount increment
|
||||
}
|
||||
} finally {
|
||||
clearInterval(checkInterval);
|
||||
const validResults = batchResults.filter(
|
||||
(result): result is NonNullable<typeof result> => result !== null
|
||||
);
|
||||
results.push(...validResults);
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -363,7 +326,7 @@ class VaultIndexer {
|
||||
}
|
||||
}
|
||||
|
||||
export { VaultIndexer, Cache, InMemoryCache, CancellationToken };
|
||||
export { VaultIndexer, Cache, InMemoryCache };
|
||||
|
||||
// Convenience method to create a VaultIndexer with an in-memory cache
|
||||
export function createVaultIndexerWithCache(vault: VaultLike): VaultIndexer {
|
||||
|
||||
@@ -190,9 +190,6 @@ describe('VaultIndexer', () => {
|
||||
const results = await indexer.searchVault('algorithm', 5);
|
||||
expect(results.length).toBe(2);
|
||||
// File with heading match should score higher
|
||||
console.log('Results:', JSON.stringify(results, null, 2));
|
||||
console.log('file1 content:', mockVault.read({ basename: 'file1', path: 'file1.md' }));
|
||||
console.log('file2 content:', mockVault.read({ basename: 'file2', path: 'file2.md' }));
|
||||
expect(results[0].title).toBe('file1');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user