Add Ollama model selector dropdown and improve automatic tool detection
- Add `listModels()`, `setModel()`, and `getModel()` methods to `OllamaClient` - Populate model dropdown in chat view from `/api/tags` endpoint - Synchronize model selection across main and agent clients - Detect user intent for vault operations from message content - Expand action phrase matching for `shouldAutoRunReadTools()` - Streamline retry logic and suppress "Let me..." text with actual tool calls - Improve fallback search query generation by removing more filler words
This commit is contained in:
@@ -8508,6 +8508,43 @@ var OllamaClient = class {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
async listModels() {
|
||||
try {
|
||||
const response = await this.fetchFn(`${this.baseURL}/api/tags`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new ApiError(`Ollama API error: ${response.status}`, response.status);
|
||||
}
|
||||
const data = await response.json();
|
||||
if (typeof data === "object" && data !== null && "models" in data && Array.isArray(data.models)) {
|
||||
const models = data.models;
|
||||
return models.filter(
|
||||
(m) => typeof m === "object" && m !== null && "name" in m && typeof m.name === "string"
|
||||
).map((m) => ({
|
||||
name: m.name,
|
||||
size: m.size,
|
||||
modified: m.modified_at
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
Logger.warn(
|
||||
`Failed to list models: ${error instanceof Error ? error.message : String(error)}`,
|
||||
"ollama-client"
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
setModel(model) {
|
||||
this.model = model;
|
||||
}
|
||||
getModel() {
|
||||
return this.model;
|
||||
}
|
||||
};
|
||||
|
||||
// src/vault-indexer.ts
|
||||
@@ -10887,6 +10924,7 @@ var ChatView = class extends import_obsidian5.ItemView {
|
||||
this.newChatButtonClickWrapper = null;
|
||||
this.listenersAttached = false;
|
||||
this.modeSelectorEl = null;
|
||||
this.modelSelectorEl = null;
|
||||
this.historySelectEl = null;
|
||||
this.historyDeleteButton = null;
|
||||
this.pendingActions = [];
|
||||
@@ -11117,6 +11155,20 @@ var ChatView = class extends import_obsidian5.ItemView {
|
||||
} else {
|
||||
newChatContainer.appendChild(this.modeSelectorEl);
|
||||
}
|
||||
if (!this.modelSelectorEl) {
|
||||
this.modelSelectorEl = newChatContainer.createEl("select", {
|
||||
cls: "ollama-model-selector"
|
||||
});
|
||||
this.modelSelectorEl.addEventListener("change", () => {
|
||||
const selectedModel = this.modelSelectorEl.value;
|
||||
this.ollamaClient.setModel(selectedModel);
|
||||
if (this.agentOllamaClient !== this.ollamaClient) {
|
||||
this.agentOllamaClient.setModel(selectedModel);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.populateModelDropdown();
|
||||
newChatContainer.appendChild(this.modelSelectorEl);
|
||||
if (!this.historySelectEl) {
|
||||
this.historySelectEl = newChatContainer.createEl("select", {
|
||||
cls: "ollama-history-selector"
|
||||
@@ -11375,6 +11427,36 @@ var ChatView = class extends import_obsidian5.ItemView {
|
||||
this.historySelectEl.value = "__new__";
|
||||
}
|
||||
}
|
||||
async populateModelDropdown() {
|
||||
if (!this.modelSelectorEl) return;
|
||||
const previousValue = this.modelSelectorEl.value;
|
||||
this.modelSelectorEl.innerHTML = "";
|
||||
const models = await this.ollamaClient.listModels();
|
||||
const currentModel = this.ollamaClient.getModel();
|
||||
if (models.length === 0) {
|
||||
const option = this.modelSelectorEl.createEl("option", {
|
||||
text: currentModel,
|
||||
attr: { value: currentModel }
|
||||
});
|
||||
option.setAttribute("selected", "selected");
|
||||
return;
|
||||
}
|
||||
for (const model of models) {
|
||||
const displayName = model.name;
|
||||
const option = this.modelSelectorEl.createEl("option", {
|
||||
text: displayName,
|
||||
attr: { value: model.name }
|
||||
});
|
||||
if (model.name === currentModel || model.name === previousValue) {
|
||||
option.setAttribute("selected", "selected");
|
||||
}
|
||||
}
|
||||
if (previousValue && models.some((m) => m.name === previousValue)) {
|
||||
this.modelSelectorEl.value = previousValue;
|
||||
} else if (models.some((m) => m.name === currentModel)) {
|
||||
this.modelSelectorEl.value = currentModel;
|
||||
}
|
||||
}
|
||||
updateMessageById(id, updates) {
|
||||
const index = this.messages.findIndex((m) => m.id === id);
|
||||
if (index !== -1) {
|
||||
@@ -12053,13 +12135,21 @@ ${actualMessage}` : actualMessage;
|
||||
let shouldFallbackToReadTools = false;
|
||||
const toolCapableModes = ["edit", "organize", "research"];
|
||||
const isToolCapable = toolCapableModes.includes(this.currentAgentMode);
|
||||
if (isToolCapable && toolCalls.length === 0 && fullResponse.trim().length > 0) {
|
||||
shouldFallbackToReadTools = this.shouldAutoRunReadTools(fullResponse);
|
||||
if (shouldFallbackToReadTools) {
|
||||
const userWantsVaultOps = this.userMessageImpliesToolUse(actualMessage);
|
||||
const modelMentionedActions = isToolCapable && this.shouldAutoRunReadTools(fullResponse);
|
||||
if (isToolCapable && toolCalls.length === 0) {
|
||||
if (modelMentionedActions || userWantsVaultOps) {
|
||||
shouldFallbackToReadTools = true;
|
||||
fullResponse = "";
|
||||
this.updateMessageById(assistantMessageId, {
|
||||
content: "",
|
||||
isStreaming: true,
|
||||
isThinking: false
|
||||
});
|
||||
let attempts = 0;
|
||||
const maxAttempts = 3;
|
||||
let currentMessages = [...messagesWithMemory];
|
||||
let currentResponse = fullResponse;
|
||||
let currentResponse = "";
|
||||
while (attempts < maxAttempts && toolCalls.length === 0) {
|
||||
attempts++;
|
||||
this.showActivityIndicator(
|
||||
@@ -12100,25 +12190,20 @@ ${actualMessage}` : actualMessage;
|
||||
}
|
||||
currentMessages = nudgeMessages;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isToolCapable && toolCalls.length === 0) {
|
||||
if (!shouldFallbackToReadTools) {
|
||||
shouldFallbackToReadTools = this.shouldAutoRunReadTools(fullResponse);
|
||||
}
|
||||
if (shouldFallbackToReadTools) {
|
||||
const autoToolCalls = this.buildAutomaticReadToolCalls(actualMessage, tools);
|
||||
if (autoToolCalls.length > 0) {
|
||||
this.showActivityIndicator("Using tools\u2026");
|
||||
toolCalls = autoToolCalls;
|
||||
fullResponse = "";
|
||||
await this.processToolCalls(
|
||||
autoToolCalls,
|
||||
messagesWithMemory,
|
||||
tools,
|
||||
fullResponse,
|
||||
assistantMessageId
|
||||
);
|
||||
if (toolCalls.length === 0) {
|
||||
const autoToolCalls = this.buildAutomaticReadToolCalls(actualMessage, tools);
|
||||
if (autoToolCalls.length > 0) {
|
||||
this.showActivityIndicator("Using tools\u2026");
|
||||
toolCalls = autoToolCalls;
|
||||
fullResponse = "";
|
||||
await this.processToolCalls(
|
||||
autoToolCalls,
|
||||
messagesWithMemory,
|
||||
tools,
|
||||
fullResponse,
|
||||
assistantMessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12188,14 +12273,52 @@ ${actualMessage}` : actualMessage;
|
||||
"let me",
|
||||
"i will",
|
||||
"i need to",
|
||||
"i should",
|
||||
"search for",
|
||||
"look for",
|
||||
"read",
|
||||
"explore",
|
||||
"check"
|
||||
"check",
|
||||
"find",
|
||||
"analyze",
|
||||
"examine",
|
||||
"review",
|
||||
"inspect",
|
||||
"investigate",
|
||||
"scan"
|
||||
];
|
||||
return intentPhrases.some((phrase) => lowerResponse.includes(phrase));
|
||||
}
|
||||
userMessageImpliesToolUse(message) {
|
||||
const toolCapableModes = ["edit", "organize", "research"];
|
||||
if (!toolCapableModes.includes(this.currentAgentMode)) {
|
||||
return false;
|
||||
}
|
||||
const lower = message.toLowerCase();
|
||||
const operationPhrases = [
|
||||
"organize",
|
||||
"structure",
|
||||
"folder",
|
||||
"tag",
|
||||
"move",
|
||||
"rename",
|
||||
"create",
|
||||
"delete",
|
||||
"search",
|
||||
"find",
|
||||
"look for",
|
||||
"read",
|
||||
"show me",
|
||||
"list",
|
||||
"what is in",
|
||||
"what are",
|
||||
"tell me about",
|
||||
"vault",
|
||||
"notes",
|
||||
"files"
|
||||
];
|
||||
return operationPhrases.some((phrase) => lower.includes(phrase));
|
||||
}
|
||||
buildAutomaticReadToolCalls(message, tools) {
|
||||
const availableTools = new Set(tools.map((tool) => tool.function.name));
|
||||
const lowerMessage = message.toLowerCase();
|
||||
@@ -12232,10 +12355,10 @@ ${actualMessage}` : actualMessage;
|
||||
}
|
||||
buildAutomaticSearchQuery(message) {
|
||||
const lowerMessage = message.toLowerCase().trim();
|
||||
if (lowerMessage === "continue" || lowerMessage === "please continue") {
|
||||
return "folder structure tags organization vault index prompts";
|
||||
if (lowerMessage === "continue" || lowerMessage === "please continue" || lowerMessage === "go ahead") {
|
||||
return "folder structure tags organization vault index";
|
||||
}
|
||||
return message.replace(/\bplease\b/gi, "").replace(/\bcontinue\b/gi, "").replace(/\bimplement\b/gi, "").replace(/\bcreate\b/gi, "").replace(/\bmove\b/gi, "").replace(/\bnotes?\b/gi, "").replace(/\bfolders?\b/gi, "").replace(/\bstructure\b/gi, "").replace(/\s+/g, " ").trim();
|
||||
return message.replace(/\bplease\b/gi, "").replace(/\bcontinue\b/gi, "").replace(/\bgo ahead\b/gi, "").replace(/\bimplement\b/gi, "").replace(/\bmove\b/gi, "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
showActivityIndicator(text) {
|
||||
if (!this.activityIndicatorEl) return;
|
||||
|
||||
+133
-33
@@ -301,6 +301,22 @@ export class ChatView extends ItemView {
|
||||
newChatContainer.appendChild(this.modeSelectorEl);
|
||||
}
|
||||
|
||||
// Setup model selector
|
||||
if (!this.modelSelectorEl) {
|
||||
this.modelSelectorEl = newChatContainer.createEl('select', {
|
||||
cls: 'ollama-model-selector',
|
||||
});
|
||||
this.modelSelectorEl.addEventListener('change', () => {
|
||||
const selectedModel = this.modelSelectorEl!.value;
|
||||
this.ollamaClient.setModel(selectedModel);
|
||||
if (this.agentOllamaClient !== this.ollamaClient) {
|
||||
this.agentOllamaClient.setModel(selectedModel);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.populateModelDropdown();
|
||||
newChatContainer.appendChild(this.modelSelectorEl);
|
||||
|
||||
// Setup chat history selector
|
||||
if (!this.historySelectEl) {
|
||||
this.historySelectEl = newChatContainer.createEl('select', {
|
||||
@@ -619,6 +635,43 @@ export class ChatView extends ItemView {
|
||||
}
|
||||
}
|
||||
|
||||
private async populateModelDropdown(): Promise<void> {
|
||||
if (!this.modelSelectorEl) return;
|
||||
const previousValue = this.modelSelectorEl.value;
|
||||
this.modelSelectorEl.innerHTML = '';
|
||||
|
||||
const models = await this.ollamaClient.listModels();
|
||||
const currentModel = this.ollamaClient.getModel();
|
||||
|
||||
if (models.length === 0) {
|
||||
// Fallback: use the configured model name if API is unavailable
|
||||
const option = this.modelSelectorEl.createEl('option', {
|
||||
text: currentModel,
|
||||
attr: { value: currentModel },
|
||||
});
|
||||
option.setAttribute('selected', 'selected');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const model of models) {
|
||||
const displayName = model.name;
|
||||
const option = this.modelSelectorEl.createEl('option', {
|
||||
text: displayName,
|
||||
attr: { value: model.name },
|
||||
});
|
||||
if (model.name === currentModel || model.name === previousValue) {
|
||||
option.setAttribute('selected', 'selected');
|
||||
}
|
||||
}
|
||||
|
||||
// If the previously selected value is still valid, keep it
|
||||
if (previousValue && models.some((m) => m.name === previousValue)) {
|
||||
this.modelSelectorEl.value = previousValue;
|
||||
} else if (models.some((m) => m.name === currentModel)) {
|
||||
this.modelSelectorEl.value = currentModel;
|
||||
}
|
||||
}
|
||||
|
||||
updateMessageById(id: string, updates: Partial<ChatMessage>): void {
|
||||
const index = this.messages.findIndex((m) => m.id === id);
|
||||
if (index !== -1) {
|
||||
@@ -1407,15 +1460,27 @@ export class ChatView extends ItemView {
|
||||
const toolCapableModes: AgentMode[] = ['edit', 'organize', 'research'];
|
||||
const isToolCapable = toolCapableModes.includes(this.currentAgentMode);
|
||||
|
||||
if (isToolCapable && toolCalls.length === 0 && fullResponse.trim().length > 0) {
|
||||
shouldFallbackToReadTools = this.shouldAutoRunReadTools(fullResponse);
|
||||
// Determine if automatic tools should run based on model response OR user intent
|
||||
const userWantsVaultOps = this.userMessageImpliesToolUse(actualMessage);
|
||||
const modelMentionedActions = isToolCapable && this.shouldAutoRunReadTools(fullResponse);
|
||||
|
||||
// If the model output action-text without tools, suppress it and retry up to 3 times
|
||||
if (shouldFallbackToReadTools) {
|
||||
if (isToolCapable && toolCalls.length === 0) {
|
||||
if (modelMentionedActions || userWantsVaultOps) {
|
||||
shouldFallbackToReadTools = true;
|
||||
|
||||
// Suppress the model's "Let me..." text — replace with actual tool execution
|
||||
fullResponse = '';
|
||||
this.updateMessageById(assistantMessageId, {
|
||||
content: '',
|
||||
isStreaming: true,
|
||||
isThinking: false,
|
||||
});
|
||||
|
||||
// Try aggressive retry loop first (hidden from user)
|
||||
let attempts = 0;
|
||||
const maxAttempts = 3;
|
||||
let currentMessages: OllamaMessage[] = [...messagesWithMemory];
|
||||
let currentResponse = fullResponse;
|
||||
let currentResponse = '';
|
||||
|
||||
while (attempts < maxAttempts && toolCalls.length === 0) {
|
||||
attempts++;
|
||||
@@ -1423,7 +1488,6 @@ export class ChatView extends ItemView {
|
||||
attempts === 1 ? 'Thinking…' : `Retrying (${attempts}/${maxAttempts})…`
|
||||
);
|
||||
|
||||
// Replace the assistant's text-only response with a forced instruction
|
||||
const nudgeMessages: OllamaMessage[] = [
|
||||
...currentMessages,
|
||||
{ role: 'assistant', content: currentResponse },
|
||||
@@ -1464,30 +1528,24 @@ export class ChatView extends ItemView {
|
||||
break;
|
||||
}
|
||||
|
||||
// If still no tools, continue the loop with the new response as context
|
||||
currentMessages = nudgeMessages;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback: if the model NEVER emitted tools but clearly intended to, force automatic read tools
|
||||
if (isToolCapable && toolCalls.length === 0) {
|
||||
if (!shouldFallbackToReadTools) {
|
||||
shouldFallbackToReadTools = this.shouldAutoRunReadTools(fullResponse);
|
||||
}
|
||||
if (shouldFallbackToReadTools) {
|
||||
const autoToolCalls = this.buildAutomaticReadToolCalls(actualMessage, tools);
|
||||
if (autoToolCalls.length > 0) {
|
||||
this.showActivityIndicator('Using tools…');
|
||||
toolCalls = autoToolCalls;
|
||||
fullResponse = '';
|
||||
await this.processToolCalls(
|
||||
autoToolCalls,
|
||||
messagesWithMemory,
|
||||
tools,
|
||||
fullResponse,
|
||||
assistantMessageId
|
||||
);
|
||||
// If retries all failed, force automatic read tools immediately
|
||||
if (toolCalls.length === 0) {
|
||||
const autoToolCalls = this.buildAutomaticReadToolCalls(actualMessage, tools);
|
||||
if (autoToolCalls.length > 0) {
|
||||
this.showActivityIndicator('Using tools…');
|
||||
toolCalls = autoToolCalls;
|
||||
fullResponse = '';
|
||||
await this.processToolCalls(
|
||||
autoToolCalls,
|
||||
messagesWithMemory,
|
||||
tools,
|
||||
fullResponse,
|
||||
assistantMessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1573,15 +1631,55 @@ export class ChatView extends ItemView {
|
||||
'let me',
|
||||
'i will',
|
||||
'i need to',
|
||||
'i should',
|
||||
'search for',
|
||||
'look for',
|
||||
'read',
|
||||
'explore',
|
||||
'check',
|
||||
'find',
|
||||
'analyze',
|
||||
'examine',
|
||||
'review',
|
||||
'inspect',
|
||||
'investigate',
|
||||
'scan',
|
||||
];
|
||||
return intentPhrases.some((phrase) => lowerResponse.includes(phrase));
|
||||
}
|
||||
|
||||
private userMessageImpliesToolUse(message: string): boolean {
|
||||
const toolCapableModes: AgentMode[] = ['edit', 'organize', 'research'];
|
||||
if (!toolCapableModes.includes(this.currentAgentMode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lower = message.toLowerCase();
|
||||
const operationPhrases = [
|
||||
'organize',
|
||||
'structure',
|
||||
'folder',
|
||||
'tag',
|
||||
'move',
|
||||
'rename',
|
||||
'create',
|
||||
'delete',
|
||||
'search',
|
||||
'find',
|
||||
'look for',
|
||||
'read',
|
||||
'show me',
|
||||
'list',
|
||||
'what is in',
|
||||
'what are',
|
||||
'tell me about',
|
||||
'vault',
|
||||
'notes',
|
||||
'files',
|
||||
];
|
||||
return operationPhrases.some((phrase) => lower.includes(phrase));
|
||||
}
|
||||
|
||||
private buildAutomaticReadToolCalls(message: string, tools: OllamaTool[]): OllamaToolCall[] {
|
||||
const availableTools = new Set(tools.map((tool) => tool.function.name));
|
||||
const lowerMessage = message.toLowerCase();
|
||||
@@ -1629,19 +1727,20 @@ export class ChatView extends ItemView {
|
||||
|
||||
private buildAutomaticSearchQuery(message: string): string {
|
||||
const lowerMessage = message.toLowerCase().trim();
|
||||
if (lowerMessage === 'continue' || lowerMessage === 'please continue') {
|
||||
return 'folder structure tags organization vault index prompts';
|
||||
if (
|
||||
lowerMessage === 'continue' ||
|
||||
lowerMessage === 'please continue' ||
|
||||
lowerMessage === 'go ahead'
|
||||
) {
|
||||
return 'folder structure tags organization vault index';
|
||||
}
|
||||
|
||||
return message
|
||||
.replace(/\bplease\b/gi, '')
|
||||
.replace(/\bcontinue\b/gi, '')
|
||||
.replace(/\bgo ahead\b/gi, '')
|
||||
.replace(/\bimplement\b/gi, '')
|
||||
.replace(/\bcreate\b/gi, '')
|
||||
.replace(/\bmove\b/gi, '')
|
||||
.replace(/\bnotes?\b/gi, '')
|
||||
.replace(/\bfolders?\b/gi, '')
|
||||
.replace(/\bstructure\b/gi, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
@@ -1675,6 +1774,7 @@ export class ChatView extends ItemView {
|
||||
private vectorStore?: VaultVectorStore;
|
||||
|
||||
private modeSelectorEl: HTMLSelectElement | null = null;
|
||||
private modelSelectorEl: HTMLSelectElement | null = null;
|
||||
private historySelectEl: HTMLSelectElement | null = null;
|
||||
private historyDeleteButton: HTMLElement | null = null;
|
||||
private currentAgentMode: AgentMode;
|
||||
|
||||
@@ -401,4 +401,57 @@ export class OllamaClient {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async listModels(): Promise<{ name: string; size?: number; modified?: string }[]> {
|
||||
try {
|
||||
const response = await this.fetchFn(`${this.baseURL}/api/tags`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ApiError(`Ollama API error: ${response.status}`, response.status);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as unknown;
|
||||
if (
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
'models' in data &&
|
||||
Array.isArray((data as { models: unknown }).models)
|
||||
) {
|
||||
const models = (data as { models: unknown[] }).models;
|
||||
return models
|
||||
.filter(
|
||||
(m): m is { name: string; size?: number; modified_at?: string } =>
|
||||
typeof m === 'object' &&
|
||||
m !== null &&
|
||||
'name' in m &&
|
||||
typeof (m as { name: unknown }).name === 'string'
|
||||
)
|
||||
.map((m) => ({
|
||||
name: m.name,
|
||||
size: m.size,
|
||||
modified: m.modified_at,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
Logger.warn(
|
||||
`Failed to list models: ${error instanceof Error ? error.message : String(error)}`,
|
||||
'ollama-client'
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
setModel(model: string): void {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
getModel(): string {
|
||||
return this.model;
|
||||
}
|
||||
}
|
||||
|
||||
+19
@@ -314,6 +314,25 @@
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
/* Model Selector */
|
||||
.ollama-model-selector {
|
||||
padding: var(--size-4-1) var(--size-4-2);
|
||||
border-radius: var(--ollama-radius);
|
||||
border: 1px solid var(--ollama-border);
|
||||
background-color: var(--background-modifier-form-field);
|
||||
color: var(--text-normal);
|
||||
font-size: var(--font-ui-small);
|
||||
cursor: pointer;
|
||||
max-width: 10rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ollama-model-selector:focus {
|
||||
outline: none;
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
/* Chat History Selector */
|
||||
.ollama-history-selector {
|
||||
padding: var(--size-4-1) var(--size-4-2);
|
||||
|
||||
Reference in New Issue
Block a user