feat(llm): use tool-based approach for reading current note

This commit is contained in:
Elian Doran
2026-03-30 17:08:47 +03:00
parent 0fc62dda78
commit 41f3274c7e
2 changed files with 45 additions and 33 deletions

View File

@@ -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<string, ModelPricing> = 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);
}

View File

@@ -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.
*/