Conversation
Add Ollama as a new LLM provider using the OpenAI-compatible API. - Create ollama.ts provider with custom baseURL pointing to local Ollama - Update AddProviderModal to include Ollama in the provider type dropdown - Register Ollama provider in the LLM service index - Handle chat title generation for Ollama models
Add new LLM tools for note management: - rename_note: Rename an existing note - delete_note: Delete a note (move to trash) - move_note: Move a note to a different parent - clone_note: Clone a note to another location All tools are marked with mutates: true for the approval system.
Implement a human-in-the-loop approval mechanism for LLM tools that modify notes. Tools marked with mutates: true require explicit user approval before execution. Server-side: - tool_registry: omit execute fn for mutating tools (AI SDK won't auto-run) - stream: flag tool calls with requiresApproval in SSE chunks - llm_chat route: new POST /api/llm-chat/execute-tool endpoint - Wrap tool execution in sql.transactional() for safety Client-side: - ToolCallCard: approve/reject buttons with visual indicators - ChatMessage/LlmChat/SidebarChat: thread approval callbacks - ExpandableCard: auto-expand for pending approvals Protocol: - LlmStreamChunk tool_use gains requiresApproval field - New ToolCall type fields: requiresApproval, rejected
Knowledge Base: - Allow users to select notes as context sources for LLM chat - Harvard-style numbered citations [1], [2] with References section - KB toggle in ChatInputBar with note selector panel Stop Generation: - AbortController-based stream cancellation - Send button toggles to stop button during streaming - Partial content preserved when generation is stopped Stream abort: - llm_chat service accepts AbortSignal for fetch cancellation - useLlmChat manages AbortController lifecycle - Graceful handling of AbortError to finalize partial messages
Web Search Engines: - Support Tavily API and self-hosted SearXNG as alternatives to provider-built-in web search - New settings UI with engine selector and conditional config fields - Tavily: API key auth, returns AI-generated answer + search results - SearXNG: self-hosted metasearch, no API key required Search Timeout: - Configurable timeout (default 15s) for web search requests - AbortSignal.timeout() applied to all custom search tool fetch calls - Timeout setting exposed in LLM settings UI (1-120 seconds) New options: llmWebSearchEngine, llmTavilyApiKey, llmSearxngUrl, llmSearchTimeout
- Rewrite AI.html with comprehensive feature documentation - Add Knowledge Base documentation page (AI/Knowledge Base.html) - Update !!!meta.json for new documentation structure - Add translation keys for: tool approval (approve, reject, pending_approval, rejected_by_user), stop generation, web search settings (engine, timeout, Tavily, SearXNG), and LLM tool names
There was a problem hiding this comment.
Code Review
This pull request significantly expands the AI Chat capabilities by introducing a Knowledge Base mode for source-specific querying, support for local Ollama models, and alternative web search engines like Tavily and SearXNG. It also implements a human-in-the-loop approval workflow for mutating tool calls (e.g., renaming or deleting notes) and adds a stop button to cancel active streams. Feedback includes a performance concern regarding markdown conversion for large note previews, a missing timeout in the Ollama fetch call, and a lack of title validation in the note renaming tool.
| const full = note.isContentAvailable() ? (() => { | ||
| const content = note.getContent(); | ||
| if (typeof content !== "string") return preview; | ||
| if (note.type === "text") { | ||
| return markdownExport.toMarkdown(content); | ||
| } | ||
| return content; | ||
| })() : preview; |
There was a problem hiding this comment.
Calling markdownExport.toMarkdown(content) on the full content of a note just to extract a 1500-character preview can be very inefficient for large notes. Since this is executed for up to 20 source notes in the knowledge base, it could significantly increase latency and memory usage. Consider truncating the HTML content before conversion or using a simpler tag-stripping method for the preview.
| */ | ||
| async loadModels(): Promise<ModelInfo[]> { | ||
| try { | ||
| const res = await fetch(`${this.baseUrl}/api/tags`); |
There was a problem hiding this comment.
The fetch call to the local Ollama instance lacks a timeout. If the service is unresponsive, this could hang the server-side request indefinitely. It's recommended to add a timeout using AbortSignal.timeout().
| const res = await fetch(`${this.baseUrl}/api/tags`); | |
| const res = await fetch(`${this.baseUrl}/api/tags`, { signal: AbortSignal.timeout(5000) }); |
| execute: ({ noteId, newTitle }) => { | ||
| const note = becca.getNote(noteId); | ||
| if (!note) { | ||
| return { error: "Note not found" }; | ||
| } | ||
| if (note.isProtected) { | ||
| return { error: "Note is protected and cannot be renamed" }; | ||
| } | ||
|
|
||
| note.title = newTitle; | ||
| note.save(); |
There was a problem hiding this comment.
The rename_note tool does not validate the newTitle. Providing an empty or whitespace-only string could lead to notes with no visible title, which is generally discouraged. It's better to add a check for empty titles.
execute: ({ noteId, newTitle }) => {
const note = becca.getNote(noteId);
if (!note) {
return { error: "Note not found" };
}
if (note.isProtected) {
return { error: "Note is protected and cannot be renamed" };
}
const trimmedTitle = newTitle.trim();
if (!trimmedTitle) {
return { error: "Title cannot be empty" };
}
note.title = trimmedTitle;
note.save();|
Hi, interesting contribution. Did you test it thoroughly? Which models were used to test Ollama? |
- base_provider: replace full markdown conversion with simple HTML tag stripping for KB previews — avoids expensive toMarkdown() call on large notes (up to 20 sources × full content) - ollama: add AbortSignal.timeout(5000) to loadModels() fetch call to prevent indefinite hangs when the Ollama service is unresponsive - note_tools: validate newTitle in rename_note — trim whitespace and reject empty titles
The note autocomplete dropdown in the Knowledge Base panel was opening downward, causing suggestions to overflow off-screen. Use a container ref and CSS to position the dropdown above the input field instead.
Add closeOnBlur option to note autocomplete Options interface. When set, skips debug:true so the dropdown closes when the input loses focus. Used in the KB note picker.
|
Hi, thanks for the review! Testing summary:
The core issue: None of the tested local models can autonomously chain multiple tool calls. Even with That said, when models do work, the results are solid - search, content retrieval, and tool execution all function correctly. This is intentionally a foundational implementation: the architecture (tool registry, approval system, streaming, knowledge base, web search) is designed to be extensible. Prompt tuning, custom system prompts, and model-specific behavior can be iterated on incrementally without architectural changes. On the security side - the tool approval system works reliably regardless of model quality. I wasn't able to get any of the tested models to bypass the approval mechanism for mutating operations, so the safety layer holds up even with less capable models. I've also addressed all three code review findings in commit
|
|
@Kureii , could you please try to separate the features into multiple PRs? There are quite a few overlapping concerns at the same time:
And anything you can think of splitting. This makes it easy for me to review and test, and perhaps not all features can go into Trilium without further changes. |
PR Title
feat(llm): enhance LLM chat with Ollama, tool approval, knowledge base, web search, and stop generation
PR Description
Summary
This PR adds several major enhancements to the LLM chat feature:
Features in Detail
Ollama Provider
@ai-sdk/openaiwith custombaseURLpointing to local Ollama instanceNote Mutation Tools
rename_note- Rename an existing notedelete_note- Delete a note (move to trash)move_note- Move a note to a different parent branchclone_note- Clone a note to another location in the treemutates: truefor the approval systemTool Approval System
mutates: truerequire explicit user approval before executiontool()withoutexecutefunction prevents auto-executionPOST /api/llm-chat/execute-toolendpoint wraps execution insql.transactional()ToolCallCardLlmStreamChunkprotocol (requiresApprovalfield)Knowledge Base Mode
[1],[2]with a References sectionChatInputBarwith note selector panelStop Generation
AbortController-based stream cancellation viafetch({ signal })AbortErrorhandling finalizes the message with collected contentWeb Search Engines
Search Timeout
AbortSignal.timeout()applied to Tavily and SearXNG fetch callsNew Options
llmWebSearchEnginestring"provider"llmTavilyApiKeystring""llmSearxngUrlstring""llmSearchTimeoutstring"15"New API Endpoints
POST/api/llm-chat/execute-toolFiles Changed (29 files, +1029 / -99)
Server
apps/server/src/services/llm/providers/ollama.ts- New: Ollama providerapps/server/src/services/llm/web_search_tools.ts- New: Tavily & SearXNG toolsapps/server/src/services/llm/providers/base_provider.ts- KB citations, web search engine selection, timeoutapps/server/src/services/llm/tools/tool_registry.ts- Approval mechanism,getMutatingToolNames()apps/server/src/services/llm/tools/note_tools.ts- rename_note, delete_noteapps/server/src/services/llm/tools/hierarchy_tools.ts- move_note, clone_noteapps/server/src/services/llm/stream.ts-requiresApprovalflag in tool-call chunksapps/server/src/routes/api/llm_chat.ts- execute-tool endpoint, mutatingToolNamesapps/server/src/routes/routes.ts- Route registrationapps/server/src/services/llm/index.ts- Ollama registrationapps/server/src/services/llm/chat_title.ts- Ollama model handlingapps/server/src/services/options_init.ts- New option defaultsapps/server/src/routes/api/options.ts- Options whitelistClient
apps/client/src/widgets/type_widgets/llm_chat/useLlmChat.ts- KB, abort, approval callbacksapps/client/src/widgets/type_widgets/llm_chat/ChatInputBar.tsx- KB toggle, stop buttonapps/client/src/widgets/type_widgets/llm_chat/ChatInputBar.css- Stop button stylingapps/client/src/widgets/type_widgets/llm_chat/ToolCallCard.tsx- Approval UIapps/client/src/widgets/type_widgets/llm_chat/ToolCallCard.css- Approval stylingapps/client/src/widgets/type_widgets/llm_chat/ChatMessage.tsx- Approval props threadingapps/client/src/widgets/type_widgets/llm_chat/LlmChat.tsx- Approval callbacksapps/client/src/widgets/type_widgets/llm_chat/ExpandableCard.tsx-defaultExpandedpropapps/client/src/widgets/type_widgets/llm_chat/llm_chat_types.ts- ToolCall type extensionsapps/client/src/widgets/sidebar/SidebarChat.tsx- Approval callback forwardingapps/client/src/services/llm_chat.ts- AbortSignal, executeToolCall()apps/client/src/widgets/type_widgets/options/llm.tsx- WebSearchSettings, timeout fieldapps/client/src/widgets/type_widgets/options/llm/AddProviderModal.tsx- Ollama optionShared
packages/commons/src/lib/llm_api.ts-requiresApprovalon tool_use chunkpackages/commons/src/lib/options_interface.ts- New option typesDocumentation & i18n
apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI.html- Rewrittenapps/server/src/assets/doc_notes/en/User Guide/User Guide/AI/Knowledge Base.html- Newapps/server/src/assets/doc_notes/en/User Guide/!!!meta.json- Updated structureapps/client/src/translations/en/translation.json- New translation keysTesting
pnpm typecheck- passespnpm client:build- builds successfully