From 41f3274c7e786097b0d21a559ea2f91c169c586e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 30 Mar 2026 17:08:47 +0300 Subject: [PATCH] feat(llm): use tool-based approach for reading current note --- .../src/services/llm/providers/anthropic.ts | 48 ++++++------------- apps/server/src/services/llm/tools.ts | 30 ++++++++++++ 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/apps/server/src/services/llm/providers/anthropic.ts b/apps/server/src/services/llm/providers/anthropic.ts index e7d1b42a41..ec6cae2fc3 100644 --- a/apps/server/src/services/llm/providers/anthropic.ts +++ b/apps/server/src/services/llm/providers/anthropic.ts @@ -3,7 +3,7 @@ import { streamText, stepCountIs, type CoreMessage } from "ai"; import type { LlmMessage } from "@triliumnext/commons"; import becca from "../../../becca/becca.js"; -import { noteTools } from "../tools.js"; +import { noteTools, currentNoteTools } from "../tools.js"; import type { LlmProvider, LlmProviderConfig, ModelInfo, ModelPricing, StreamResult } from "../types.js"; const DEFAULT_MODEL = "claude-sonnet-4-6"; @@ -96,39 +96,17 @@ const MODEL_PRICING: Record = Object.fromEntries( ); /** - * Build context string from the current note being viewed. + * Build a lightweight context hint about the current note (title + type only, no content). + * The full content is available via the get_current_note tool. */ -function buildNoteContext(noteId: string): string | null { +function buildNoteHint(noteId: string): string | null { const note = becca.getNote(noteId); if (!note) { return null; } - const parts: string[] = []; - parts.push(`The user is currently viewing a note titled "${note.title}" (ID: ${noteId}).`); - - // Add note type context - if (note.type !== "text") { - parts.push(`Note type: ${note.type}`); - } - - // Add content for text notes (truncate if too long) - if (note.type === "text" || note.type === "code") { - try { - const content = note.getContent(); - if (typeof content === "string" && content.trim()) { - const maxLength = 4000; - const truncated = content.length > maxLength - ? content.substring(0, maxLength) + "\n... (content truncated)" - : content; - parts.push(`\nNote content:\n\`\`\`\n${truncated}\n\`\`\``); - } - } catch { - // Content not available - } - } - - return parts.join("\n"); + const typeInfo = note.type !== "text" ? ` (type: ${note.type})` : ""; + return `The user is currently viewing a note titled "${note.title}"${typeInfo}. Use the get_current_note tool to read its content if needed.`; } export class AnthropicProvider implements LlmProvider { @@ -146,13 +124,13 @@ export class AnthropicProvider implements LlmProvider { let systemPrompt = config.systemPrompt || messages.find(m => m.role === "system")?.content; const chatMessages = messages.filter(m => m.role !== "system"); - // Add note context if viewing a note + // Add a lightweight hint about the current note (content available via tool) if (config.contextNoteId) { - const noteContext = buildNoteContext(config.contextNoteId); - if (noteContext) { + const noteHint = buildNoteHint(config.contextNoteId); + if (noteHint) { systemPrompt = systemPrompt - ? `${systemPrompt}\n\n${noteContext}` - : noteContext; + ? `${systemPrompt}\n\n${noteHint}` + : noteHint; } } @@ -198,6 +176,10 @@ export class AnthropicProvider implements LlmProvider { }); } + if (config.contextNoteId) { + Object.assign(tools, currentNoteTools(config.contextNoteId)); + } + if (config.enableNoteTools) { Object.assign(tools, noteTools); } diff --git a/apps/server/src/services/llm/tools.ts b/apps/server/src/services/llm/tools.ts index fd6c4f8e60..5b4abded48 100644 --- a/apps/server/src/services/llm/tools.ts +++ b/apps/server/src/services/llm/tools.ts @@ -177,6 +177,36 @@ export const createNote = tool({ } }); +/** + * Read the content of the note the user is currently viewing. + * Created dynamically so it captures the contextNoteId. + */ +export function currentNoteTools(contextNoteId: string) { + return { + get_current_note: tool({ + description: "Read the content of the note the user is currently viewing. Call this when the user asks about or refers to their current note.", + inputSchema: z.object({}), + execute: async () => { + const note = becca.getNote(contextNoteId); + if (!note) { + return { error: "Note not found" }; + } + if (note.isProtected) { + return { error: "Note is protected" }; + } + + const content = note.getContent(); + return { + noteId: note.noteId, + title: note.title, + type: note.type, + content: typeof content === "string" ? content : "[binary content]" + }; + } + }) + }; +} + /** * All available note tools. */