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: {
|
parserOptions: {
|
||||||
project: null,
|
project: null,
|
||||||
},
|
},
|
||||||
@@ -37,5 +37,11 @@ module.exports = {
|
|||||||
es2020: true,
|
es2020: true,
|
||||||
jest: 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",
|
"@typescript-eslint/parser": "^6.19.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
|
"jest-environment-jsdom": "^30.3.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"ts-jest": "^29.1.2",
|
"ts-jest": "^29.1.2",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jest-environment-jsdom": "^30.3.0",
|
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"obsidian": "^1.4.11"
|
"obsidian": "^1.4.11"
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-87
@@ -2,8 +2,6 @@
|
|||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.ChatView = void 0;
|
exports.ChatView = void 0;
|
||||||
const obsidian_1 = require("obsidian");
|
const obsidian_1 = require("obsidian");
|
||||||
const DEFAULT_VAULT_SEARCH_LIMIT = 3;
|
|
||||||
const MAX_MESSAGE_HISTORY = 50;
|
|
||||||
const MAX_STREAM_CHUNKS = 1000;
|
const MAX_STREAM_CHUNKS = 1000;
|
||||||
const ollama_client_1 = require("./ollama-client");
|
const ollama_client_1 = require("./ollama-client");
|
||||||
const vault_indexer_1 = require("./vault-indexer");
|
const vault_indexer_1 = require("./vault-indexer");
|
||||||
@@ -213,7 +211,7 @@ class ChatView extends obsidian_1.ItemView {
|
|||||||
updateLastMessage(content) {
|
updateLastMessage(content) {
|
||||||
const streamingMessage = this.messages.find((msg) => msg.isStreaming);
|
const streamingMessage = this.messages.find((msg) => msg.isStreaming);
|
||||||
if (streamingMessage && !this.lastMessageEl) {
|
if (streamingMessage && !this.lastMessageEl) {
|
||||||
this.lastMessageEl = this.contentEl.createEl('div', {
|
this.lastMessageEl = (this.chatContainer ?? this.contentEl).createEl('div', {
|
||||||
cls: `ollama-message assistant`,
|
cls: `ollama-message assistant`,
|
||||||
});
|
});
|
||||||
this.lastMessageEl.setAttribute('data-msg-id', streamingMessage.id);
|
this.lastMessageEl.setAttribute('data-msg-id', streamingMessage.id);
|
||||||
@@ -222,44 +220,8 @@ class ChatView extends obsidian_1.ItemView {
|
|||||||
this.lastMessageEl.textContent = content;
|
this.lastMessageEl.textContent = content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async handleUserInput(content) {
|
getTools() {
|
||||||
if (!this.sendButton || !this.inputEl)
|
return [
|
||||||
return;
|
|
||||||
this.sendButton.disabled = true;
|
|
||||||
try {
|
|
||||||
// Guard against empty messages
|
|
||||||
const userMessage = content.trim();
|
|
||||||
if (!userMessage)
|
|
||||||
return;
|
|
||||||
// Search vault using user message as query
|
|
||||||
const entries = await this.vaultIndexer.searchVault(userMessage, DEFAULT_VAULT_SEARCH_LIMIT);
|
|
||||||
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',
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
@@ -279,51 +241,30 @@ class ChatView extends obsidian_1.ItemView {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const messageId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
}
|
||||||
const userMessageId = messageId;
|
buildMessages(userMessage, context) {
|
||||||
const assistantMessageId = `${messageId}-assistant`;
|
const systemContent = context
|
||||||
// Store user message in conversation history
|
? `You are a helpful assistant.\n\nRelevant vault context:\n${context}`
|
||||||
const userChatMessage = {
|
: 'You are a helpful assistant.';
|
||||||
id: userMessageId,
|
const systemMessage = {
|
||||||
|
role: 'system',
|
||||||
|
content: systemContent,
|
||||||
|
};
|
||||||
|
const userMessageWithContext = {
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: userMessage,
|
content: userMessage,
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
};
|
||||||
const assistantMessage = {
|
return [
|
||||||
id: assistantMessageId,
|
systemMessage,
|
||||||
role: 'assistant',
|
...this.messages.map((m) => ({
|
||||||
content: '',
|
role: m.role,
|
||||||
timestamp: Date.now(),
|
content: m.content,
|
||||||
isStreaming: true,
|
tool_calls: m.tool_calls,
|
||||||
};
|
})),
|
||||||
// Update messages immutably
|
userMessageWithContext,
|
||||||
this.messages = [...this.messages, userChatMessage, assistantMessage];
|
];
|
||||||
try {
|
|
||||||
this.render();
|
|
||||||
const stream = this.ollamaClient.streamChat(messages, tools);
|
|
||||||
let fullResponse = '';
|
|
||||||
let toolCalls = [];
|
|
||||||
let chunkCount = 0;
|
|
||||||
for await (const chunk of stream) {
|
|
||||||
chunkCount++;
|
|
||||||
if (chunkCount > MAX_STREAM_CHUNKS) {
|
|
||||||
throw new Error('Response too long, stopped streaming');
|
|
||||||
}
|
}
|
||||||
if (chunk.content) {
|
async processToolCalls(toolCalls, messages, tools, fullResponse, assistantMessageId) {
|
||||||
fullResponse += chunk.content;
|
|
||||||
}
|
|
||||||
if (chunk.tool_calls) {
|
|
||||||
toolCalls = toolCalls.concat(chunk.tool_calls);
|
|
||||||
}
|
|
||||||
this.updateLastMessage(fullResponse);
|
|
||||||
}
|
|
||||||
// Update the assistant message with the full response immutably
|
|
||||||
this.updateMessageById(assistantMessageId, {
|
|
||||||
content: fullResponse,
|
|
||||||
tool_calls: toolCalls,
|
|
||||||
});
|
|
||||||
// Process tool calls with proper follow-up context
|
|
||||||
if (toolCalls.length > 0) {
|
|
||||||
// Validate tool calls before processing
|
// Validate tool calls before processing
|
||||||
const MAX_TOOL_CALLS = 10;
|
const MAX_TOOL_CALLS = 10;
|
||||||
if (toolCalls.length > MAX_TOOL_CALLS) {
|
if (toolCalls.length > MAX_TOOL_CALLS) {
|
||||||
@@ -370,6 +311,72 @@ class ChatView extends obsidian_1.ItemView {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async handleUserInput(content) {
|
||||||
|
if (!this.sendButton || !this.inputEl)
|
||||||
|
return;
|
||||||
|
this.sendButton.disabled = true;
|
||||||
|
try {
|
||||||
|
// Guard against empty messages
|
||||||
|
const userMessage = content.trim();
|
||||||
|
if (!userMessage)
|
||||||
|
return;
|
||||||
|
// Search vault using user message as query
|
||||||
|
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 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`;
|
||||||
|
// Store user message in conversation history
|
||||||
|
const userChatMessage = {
|
||||||
|
id: userMessageId,
|
||||||
|
role: 'user',
|
||||||
|
content: userMessage,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
const assistantMessage = {
|
||||||
|
id: assistantMessageId,
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
isStreaming: true,
|
||||||
|
};
|
||||||
|
// Update messages immutably
|
||||||
|
this.messages = [...this.messages, userChatMessage, assistantMessage];
|
||||||
|
try {
|
||||||
|
this.render();
|
||||||
|
const stream = this.ollamaClient.streamChat(messages, tools);
|
||||||
|
let fullResponse = '';
|
||||||
|
let toolCalls = [];
|
||||||
|
let chunkCount = 0;
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
chunkCount++;
|
||||||
|
if (chunkCount > MAX_STREAM_CHUNKS) {
|
||||||
|
throw new Error('Response too long, stopped streaming');
|
||||||
|
}
|
||||||
|
if (chunk.content) {
|
||||||
|
fullResponse += chunk.content;
|
||||||
|
}
|
||||||
|
if (chunk.tool_calls) {
|
||||||
|
toolCalls = toolCalls.concat(chunk.tool_calls);
|
||||||
|
}
|
||||||
|
this.updateLastMessage(fullResponse);
|
||||||
|
}
|
||||||
|
// Update the assistant message with the full response immutably
|
||||||
|
this.updateMessageById(assistantMessageId, {
|
||||||
|
content: fullResponse,
|
||||||
|
tool_calls: toolCalls,
|
||||||
|
});
|
||||||
|
// Process tool calls with proper follow-up context
|
||||||
|
if (toolCalls.length > 0) {
|
||||||
|
await this.processToolCalls(toolCalls, messages, tools, fullResponse, assistantMessageId);
|
||||||
|
}
|
||||||
// Update assistant message immutably — only if no tool calls were processed
|
// Update assistant message immutably — only if no tool calls were processed
|
||||||
if (toolCalls.length === 0) {
|
if (toolCalls.length === 0) {
|
||||||
this.updateMessageById(assistantMessageId, {
|
this.updateMessageById(assistantMessageId, {
|
||||||
@@ -377,8 +384,8 @@ class ChatView extends obsidian_1.ItemView {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Limit conversation history to prevent memory issues
|
// Limit conversation history to prevent memory issues
|
||||||
if (this.messages.length > MAX_MESSAGE_HISTORY) {
|
if (this.messages.length > this.settings.maxMessageHistory) {
|
||||||
this.messages = this.messages.slice(-MAX_MESSAGE_HISTORY);
|
this.messages = this.messages.slice(-this.settings.maxMessageHistory);
|
||||||
}
|
}
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
@@ -391,11 +398,9 @@ class ChatView extends obsidian_1.ItemView {
|
|||||||
// Use centralized error handler
|
// Use centralized error handler
|
||||||
error_handler_1.ErrorHandler.handleError(error, 'ChatView.handleUserInput');
|
error_handler_1.ErrorHandler.handleError(error, 'ChatView.handleUserInput');
|
||||||
// Update any streaming messages to non-streaming state to prevent stale messages
|
// 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.messages = this.messages.map((msg) => msg.isStreaming ? { ...msg, isStreaming: false } : msg);
|
||||||
this.cleanupStreamingResources();
|
this.cleanupStreamingResources();
|
||||||
|
this.render();
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
if (this.sendButton) {
|
if (this.sendButton) {
|
||||||
|
|||||||
+119
-103
@@ -5,8 +5,6 @@ type KeyboardEvent = globalThis.KeyboardEvent;
|
|||||||
type HTMLTextAreaElement = globalThis.HTMLTextAreaElement;
|
type HTMLTextAreaElement = globalThis.HTMLTextAreaElement;
|
||||||
type HTMLButtonElement = globalThis.HTMLButtonElement;
|
type HTMLButtonElement = globalThis.HTMLButtonElement;
|
||||||
|
|
||||||
const DEFAULT_VAULT_SEARCH_LIMIT = 3;
|
|
||||||
const MAX_MESSAGE_HISTORY = 50;
|
|
||||||
const MAX_STREAM_CHUNKS = 1000;
|
const MAX_STREAM_CHUNKS = 1000;
|
||||||
import {
|
import {
|
||||||
PluginSettings,
|
PluginSettings,
|
||||||
@@ -258,7 +256,7 @@ export class ChatView extends ItemView {
|
|||||||
private updateLastMessage(content: string) {
|
private updateLastMessage(content: string) {
|
||||||
const streamingMessage = this.messages.find((msg) => msg.isStreaming);
|
const streamingMessage = this.messages.find((msg) => msg.isStreaming);
|
||||||
if (streamingMessage && !this.lastMessageEl) {
|
if (streamingMessage && !this.lastMessageEl) {
|
||||||
this.lastMessageEl = this.contentEl.createEl('div', {
|
this.lastMessageEl = (this.chatContainer ?? this.contentEl).createEl('div', {
|
||||||
cls: `ollama-message assistant`,
|
cls: `ollama-message assistant`,
|
||||||
}) as HTMLElement;
|
}) as HTMLElement;
|
||||||
this.lastMessageEl.setAttribute('data-msg-id', streamingMessage.id);
|
this.lastMessageEl.setAttribute('data-msg-id', streamingMessage.id);
|
||||||
@@ -268,51 +266,8 @@ export class ChatView extends ItemView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleUserInput(content: string) {
|
private getTools(): OllamaTool[] {
|
||||||
if (!this.sendButton || !this.inputEl) return;
|
return [
|
||||||
this.sendButton.disabled = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Guard against empty messages
|
|
||||||
const userMessage = content.trim();
|
|
||||||
if (!userMessage) return;
|
|
||||||
|
|
||||||
// Search vault using user message as query
|
|
||||||
const entries = await this.vaultIndexer.searchVault(userMessage, DEFAULT_VAULT_SEARCH_LIMIT);
|
|
||||||
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: 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',
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
@@ -332,6 +287,118 @@ export class ChatView extends ItemView {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Guard against empty messages
|
||||||
|
const userMessage = content.trim();
|
||||||
|
if (!userMessage) return;
|
||||||
|
|
||||||
|
// Search vault using user message as query
|
||||||
|
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 messages = this.buildMessages(userMessage, context);
|
||||||
|
const tools = this.getTools();
|
||||||
|
|
||||||
const messageId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
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
|
// Process tool calls with proper follow-up context
|
||||||
if (toolCalls.length > 0) {
|
if (toolCalls.length > 0) {
|
||||||
// Validate tool calls before processing
|
await this.processToolCalls(toolCalls, messages, tools, fullResponse, assistantMessageId);
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update assistant message immutably — only if no tool calls were processed
|
// 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
|
// Limit conversation history to prevent memory issues
|
||||||
if (this.messages.length > MAX_MESSAGE_HISTORY) {
|
if (this.messages.length > this.settings.maxMessageHistory) {
|
||||||
this.messages = this.messages.slice(-MAX_MESSAGE_HISTORY);
|
this.messages = this.messages.slice(-this.settings.maxMessageHistory);
|
||||||
}
|
}
|
||||||
this.render();
|
this.render();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -461,13 +479,11 @@ export class ChatView extends ItemView {
|
|||||||
// Use centralized error handler
|
// Use centralized error handler
|
||||||
ErrorHandler.handleError(error, 'ChatView.handleUserInput');
|
ErrorHandler.handleError(error, 'ChatView.handleUserInput');
|
||||||
// Update any streaming messages to non-streaming state to prevent stale messages
|
// 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) =>
|
this.messages = this.messages.map((msg) =>
|
||||||
msg.isStreaming ? { ...msg, isStreaming: false } : msg
|
msg.isStreaming ? { ...msg, isStreaming: false } : msg
|
||||||
);
|
);
|
||||||
this.cleanupStreamingResources();
|
this.cleanupStreamingResources();
|
||||||
|
this.render();
|
||||||
} finally {
|
} finally {
|
||||||
if (this.sendButton) {
|
if (this.sendButton) {
|
||||||
this.sendButton.disabled = false;
|
this.sendButton.disabled = false;
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ class OllamaClient {
|
|||||||
if (response.status >= 500 && attempt < this.maxRetries) {
|
if (response.status >= 500 && attempt < this.maxRetries) {
|
||||||
const retryDelay = Math.pow(2, attempt) * 100;
|
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');
|
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 retryTimeout = new Promise((resolve) => setTimeout(resolve, retryDelay));
|
||||||
const abortListener = () => {
|
const abortListener = () => {
|
||||||
utils_1.Logger.info('Retry aborted by user', 'ollama-client');
|
utils_1.Logger.info('Retry aborted by user', 'ollama-client');
|
||||||
@@ -75,7 +74,6 @@ class OllamaClient {
|
|||||||
else {
|
else {
|
||||||
await retryTimeout;
|
await retryTimeout;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
yield* this.streamChatWithRetry(messages, tools, attempt + 1);
|
yield* this.streamChatWithRetry(messages, tools, attempt + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -148,12 +146,16 @@ class OllamaClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
// Abort the local controller
|
// Abort the local controller to release underlying fetch resources if not already aborted
|
||||||
|
if (!controller.signal.aborted) {
|
||||||
controller.abort();
|
controller.abort();
|
||||||
}
|
}
|
||||||
// Clean up the reference
|
// Clean up the reference only if this is still the current stream
|
||||||
|
if (this.currentStreamController === controller) {
|
||||||
this.currentStreamController = null;
|
this.currentStreamController = null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
async chat(messages, tools = []) {
|
async chat(messages, tools = []) {
|
||||||
return this.chatWithRetry(messages, tools, 0);
|
return this.chatWithRetry(messages, tools, 0);
|
||||||
}
|
}
|
||||||
@@ -178,7 +180,6 @@ class OllamaClient {
|
|||||||
if (response.status >= 500 && attempt < this.maxRetries) {
|
if (response.status >= 500 && attempt < this.maxRetries) {
|
||||||
const retryDelay = Math.pow(2, attempt) * 100;
|
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');
|
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 retryTimeout = new Promise((resolve) => setTimeout(resolve, retryDelay));
|
||||||
const abortListener = () => {
|
const abortListener = () => {
|
||||||
utils_1.Logger.info('Retry aborted by user', 'ollama-client');
|
utils_1.Logger.info('Retry aborted by user', 'ollama-client');
|
||||||
@@ -203,7 +204,6 @@ class OllamaClient {
|
|||||||
else {
|
else {
|
||||||
await retryTimeout;
|
await retryTimeout;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return this.chatWithRetry(messages, tools, attempt + 1);
|
return this.chatWithRetry(messages, tools, attempt + 1);
|
||||||
}
|
}
|
||||||
throw new types_1.ApiError(`Ollama API error: ${response.status}`, response.status);
|
throw new types_1.ApiError(`Ollama API error: ${response.status}`, response.status);
|
||||||
@@ -212,10 +212,12 @@ class OllamaClient {
|
|||||||
return (this.toOllamaMessage(data.message) ?? { role: 'assistant', content: '', tool_calls: [] });
|
return (this.toOllamaMessage(data.message) ?? { role: 'assistant', content: '', tool_calls: [] });
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
// Abort the local controller
|
// Abort the local controller to release underlying fetch resources if not already aborted
|
||||||
|
if (!controller.signal.aborted) {
|
||||||
controller.abort();
|
controller.abort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
throwIfOllamaError(parsed) {
|
throwIfOllamaError(parsed) {
|
||||||
if (parsed.error) {
|
if (parsed.error) {
|
||||||
throw new Error(`Ollama error: ${String(parsed.error)}`);
|
throw new Error(`Ollama error: ${String(parsed.error)}`);
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ export class OllamaClient {
|
|||||||
`Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`,
|
`Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`,
|
||||||
'ollama-client'
|
'ollama-client'
|
||||||
);
|
);
|
||||||
if (attempt < this.maxRetries - 1) {
|
|
||||||
const retryTimeout = new Promise((resolve) => setTimeout(resolve, retryDelay));
|
const retryTimeout = new Promise((resolve) => setTimeout(resolve, retryDelay));
|
||||||
const abortListener = () => {
|
const abortListener = () => {
|
||||||
Logger.info('Retry aborted by user', 'ollama-client');
|
Logger.info('Retry aborted by user', 'ollama-client');
|
||||||
@@ -100,7 +99,6 @@ export class OllamaClient {
|
|||||||
} else {
|
} else {
|
||||||
await retryTimeout;
|
await retryTimeout;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
yield* this.streamChatWithRetry(messages, tools, attempt + 1);
|
yield* this.streamChatWithRetry(messages, tools, attempt + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -187,8 +185,10 @@ export class OllamaClient {
|
|||||||
reader.releaseLock();
|
reader.releaseLock();
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Abort the local controller
|
// Abort the local controller to release underlying fetch resources if not already aborted
|
||||||
|
if (!controller.signal.aborted) {
|
||||||
controller.abort();
|
controller.abort();
|
||||||
|
}
|
||||||
// Clean up the reference only if this is still the current stream
|
// Clean up the reference only if this is still the current stream
|
||||||
if (this.currentStreamController === controller) {
|
if (this.currentStreamController === controller) {
|
||||||
this.currentStreamController = null;
|
this.currentStreamController = null;
|
||||||
@@ -229,7 +229,6 @@ export class OllamaClient {
|
|||||||
`Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`,
|
`Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`,
|
||||||
'ollama-client'
|
'ollama-client'
|
||||||
);
|
);
|
||||||
if (attempt < this.maxRetries - 1) {
|
|
||||||
const retryTimeout = new Promise((resolve) => setTimeout(resolve, retryDelay));
|
const retryTimeout = new Promise((resolve) => setTimeout(resolve, retryDelay));
|
||||||
const abortListener = () => {
|
const abortListener = () => {
|
||||||
Logger.info('Retry aborted by user', 'ollama-client');
|
Logger.info('Retry aborted by user', 'ollama-client');
|
||||||
@@ -252,7 +251,6 @@ export class OllamaClient {
|
|||||||
} else {
|
} else {
|
||||||
await retryTimeout;
|
await retryTimeout;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return this.chatWithRetry(messages, tools, attempt + 1);
|
return this.chatWithRetry(messages, tools, attempt + 1);
|
||||||
}
|
}
|
||||||
throw new ApiError(`Ollama API error: ${response.status}`, response.status);
|
throw new ApiError(`Ollama API error: ${response.status}`, response.status);
|
||||||
@@ -263,10 +261,12 @@ export class OllamaClient {
|
|||||||
this.toOllamaMessage(data.message) ?? { role: 'assistant', content: '', tool_calls: [] }
|
this.toOllamaMessage(data.message) ?? { role: 'assistant', content: '', tool_calls: [] }
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
// Abort the local controller
|
// Abort the local controller to release underlying fetch resources if not already aborted
|
||||||
|
if (!controller.signal.aborted) {
|
||||||
controller.abort();
|
controller.abort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private throwIfOllamaError(parsed: Record<string, unknown>): void {
|
private throwIfOllamaError(parsed: Record<string, unknown>): void {
|
||||||
if (parsed.error) {
|
if (parsed.error) {
|
||||||
|
|||||||
+2
-2
@@ -147,8 +147,8 @@ function safeParseJson(jsonString) {
|
|||||||
if (dangerousKeys.some((key) => Object.keys(obj).includes(key))) {
|
if (dangerousKeys.some((key) => Object.keys(obj).includes(key))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Recursively check nested objects
|
// Recursively check nested objects (own properties only)
|
||||||
for (const key in obj) {
|
for (const key of Object.keys(obj)) {
|
||||||
if (checkDangerousPatterns(obj[key])) {
|
if (checkDangerousPatterns(obj[key])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -166,8 +166,8 @@ export function safeParseJson(jsonString: string): unknown {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively check nested objects
|
// Recursively check nested objects (own properties only)
|
||||||
for (const key in obj as Record<string, unknown>) {
|
for (const key of Object.keys(obj as Record<string, unknown>)) {
|
||||||
if (checkDangerousPatterns((obj as Record<string, unknown>)[key])) {
|
if (checkDangerousPatterns((obj as Record<string, unknown>)[key])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-36
@@ -1,7 +1,7 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// src/vault-indexer.ts
|
// src/vault-indexer.ts
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.CancellationToken = exports.InMemoryCache = exports.VaultIndexer = void 0;
|
exports.InMemoryCache = exports.VaultIndexer = void 0;
|
||||||
exports.createVaultIndexerWithCache = createVaultIndexerWithCache;
|
exports.createVaultIndexerWithCache = createVaultIndexerWithCache;
|
||||||
const utils_1 = require("./utils");
|
const utils_1 = require("./utils");
|
||||||
class InMemoryCache {
|
class InMemoryCache {
|
||||||
@@ -21,18 +21,6 @@ class InMemoryCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
exports.InMemoryCache = InMemoryCache;
|
exports.InMemoryCache = InMemoryCache;
|
||||||
class CancellationToken {
|
|
||||||
constructor() {
|
|
||||||
this.cancelled = false;
|
|
||||||
}
|
|
||||||
cancel() {
|
|
||||||
this.cancelled = true;
|
|
||||||
}
|
|
||||||
get isCancelled() {
|
|
||||||
return this.cancelled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.CancellationToken = CancellationToken;
|
|
||||||
class VaultIndexer {
|
class VaultIndexer {
|
||||||
constructor(vault, cache) {
|
constructor(vault, cache) {
|
||||||
this.vault = null;
|
this.vault = null;
|
||||||
@@ -70,10 +58,7 @@ class VaultIndexer {
|
|||||||
const vault = this.vault;
|
const vault = this.vault;
|
||||||
const allFiles = vault.getMarkdownFiles();
|
const allFiles = vault.getMarkdownFiles();
|
||||||
const results = await this.processFilesInBatches(vault, allFiles, queryTokens);
|
const results = await this.processFilesInBatches(vault, allFiles, queryTokens);
|
||||||
const filteredResults = results
|
const filteredResults = results.sort((a, b) => b.score - a.score).slice(0, limit);
|
||||||
.filter((result) => result !== null)
|
|
||||||
.sort((a, b) => b.score - a.score)
|
|
||||||
.slice(0, limit);
|
|
||||||
if (this.cache) {
|
if (this.cache) {
|
||||||
try {
|
try {
|
||||||
await this.cache.put(cacheKey, JSON.stringify(filteredResults));
|
await this.cache.put(cacheKey, JSON.stringify(filteredResults));
|
||||||
@@ -88,19 +73,7 @@ class VaultIndexer {
|
|||||||
const batchSize = 10;
|
const batchSize = 10;
|
||||||
const results = [];
|
const results = [];
|
||||||
const seenPaths = new Set();
|
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) {
|
for (let i = 0; i < files.length; i += batchSize) {
|
||||||
if (cancellationToken.isCancelled) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const batch = files.slice(i, i + batchSize);
|
const batch = files.slice(i, i + batchSize);
|
||||||
const batchResults = await Promise.all(batch.map(async (file) => {
|
const batchResults = await Promise.all(batch.map(async (file) => {
|
||||||
try {
|
try {
|
||||||
@@ -129,13 +102,6 @@ class VaultIndexer {
|
|||||||
}));
|
}));
|
||||||
const validResults = batchResults.filter((result) => result !== null);
|
const validResults = batchResults.filter((result) => result !== null);
|
||||||
results.push(...validResults);
|
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 results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-40
@@ -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 {
|
interface Frontmatter {
|
||||||
title?: string;
|
title?: string;
|
||||||
tags?: string;
|
tags?: string;
|
||||||
@@ -112,10 +100,7 @@ class VaultIndexer {
|
|||||||
const allFiles = vault.getMarkdownFiles();
|
const allFiles = vault.getMarkdownFiles();
|
||||||
const results = await this.processFilesInBatches(vault, allFiles, queryTokens);
|
const results = await this.processFilesInBatches(vault, allFiles, queryTokens);
|
||||||
|
|
||||||
const filteredResults = results
|
const filteredResults = results.sort((a, b) => b.score - a.score).slice(0, limit);
|
||||||
.filter((result): result is NonNullable<typeof result> => result !== null)
|
|
||||||
.sort((a, b) => b.score - a.score)
|
|
||||||
.slice(0, limit);
|
|
||||||
|
|
||||||
if (this.cache) {
|
if (this.cache) {
|
||||||
try {
|
try {
|
||||||
@@ -135,26 +120,12 @@ class VaultIndexer {
|
|||||||
vault: VaultLike,
|
vault: VaultLike,
|
||||||
files: VaultFile[],
|
files: VaultFile[],
|
||||||
queryTokens: string[]
|
queryTokens: string[]
|
||||||
): Promise<Array<VaultIndexEntry | null>> {
|
): Promise<VaultIndexEntry[]> {
|
||||||
const batchSize = 10;
|
const batchSize = 10;
|
||||||
const results: VaultIndexEntry[] = [];
|
const results: VaultIndexEntry[] = [];
|
||||||
const seenPaths = new Set<string>();
|
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) {
|
for (let i = 0; i < files.length; i += batchSize) {
|
||||||
if (cancellationToken.isCancelled) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const batch = files.slice(i, i + batchSize);
|
const batch = files.slice(i, i + batchSize);
|
||||||
const batchResults = await Promise.all(
|
const batchResults = await Promise.all(
|
||||||
batch.map(async (file) => {
|
batch.map(async (file) => {
|
||||||
@@ -190,14 +161,6 @@ class VaultIndexer {
|
|||||||
(result): result is NonNullable<typeof result> => result !== null
|
(result): result is NonNullable<typeof result> => result !== null
|
||||||
);
|
);
|
||||||
results.push(...validResults);
|
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 results;
|
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
|
// Convenience method to create a VaultIndexer with an in-memory cache
|
||||||
export function createVaultIndexerWithCache(vault: VaultLike): VaultIndexer {
|
export function createVaultIndexerWithCache(vault: VaultLike): VaultIndexer {
|
||||||
|
|||||||
@@ -190,9 +190,6 @@ describe('VaultIndexer', () => {
|
|||||||
const results = await indexer.searchVault('algorithm', 5);
|
const results = await indexer.searchVault('algorithm', 5);
|
||||||
expect(results.length).toBe(2);
|
expect(results.length).toBe(2);
|
||||||
// File with heading match should score higher
|
// 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');
|
expect(results[0].title).toBe('file1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user