mirror of
https://github.com/TriliumNext/Trilium.git
synced 2025-12-11 05:45:26 -06:00
243 lines
9.4 KiB
TypeScript
243 lines
9.4 KiB
TypeScript
/**
|
|
* Handler for LLM context management
|
|
* Uses TriliumNext's native search service for powerful note discovery
|
|
*/
|
|
import log from "../../../log.js";
|
|
import becca from "../../../../becca/becca.js";
|
|
import contextService from "../../context/services/context_service.js";
|
|
import searchService from "../../../search/services/search.js";
|
|
import type { NoteSource } from "../../interfaces/chat_session.js";
|
|
|
|
/**
|
|
* Handles context management for LLM chat
|
|
*/
|
|
export class ContextHandler {
|
|
/**
|
|
* Find relevant notes based on search query using TriliumNext's search service
|
|
* @param content The search content
|
|
* @param contextNoteId Optional note ID for context
|
|
* @param limit Maximum number of results to return
|
|
* @returns Array of relevant note sources
|
|
*/
|
|
static async findRelevantNotes(content: string, contextNoteId: string | null = null, limit = 5): Promise<NoteSource[]> {
|
|
try {
|
|
// If content is too short, don't bother
|
|
if (content.length < 3) {
|
|
return [];
|
|
}
|
|
|
|
log.info(`Finding relevant notes for query: "${content.substring(0, 50)}..." using TriliumNext search`);
|
|
|
|
const sources: NoteSource[] = [];
|
|
|
|
if (contextNoteId) {
|
|
// For branch context, get notes specifically from that branch and related notes
|
|
const contextNote = becca.notes[contextNoteId];
|
|
if (!contextNote) {
|
|
return [];
|
|
}
|
|
|
|
const relevantNotes = this.findNotesInContext(contextNote, content, limit);
|
|
sources.push(...relevantNotes);
|
|
} else {
|
|
// General search across all notes using TriliumNext's search service
|
|
const relevantNotes = this.findNotesBySearch(content, limit);
|
|
sources.push(...relevantNotes);
|
|
}
|
|
|
|
log.info(`Found ${sources.length} relevant notes using TriliumNext search`);
|
|
return sources.slice(0, limit);
|
|
} catch (error: any) {
|
|
log.error(`Error finding relevant notes: ${error.message}`);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find notes in the context of a specific note (children, siblings, linked notes)
|
|
*/
|
|
private static findNotesInContext(contextNote: any, searchQuery: string, limit: number): NoteSource[] {
|
|
const sources: NoteSource[] = [];
|
|
const processedNoteIds = new Set<string>();
|
|
|
|
// Add the context note itself (high priority)
|
|
sources.push(this.createNoteSource(contextNote, 1.0));
|
|
processedNoteIds.add(contextNote.noteId);
|
|
|
|
// Get child notes (search within children)
|
|
try {
|
|
const childQuery = `note.childOf.noteId = "${contextNote.noteId}" ${searchQuery}`;
|
|
const childSearchResults = searchService.searchNotes(childQuery, { includeArchivedNotes: false });
|
|
|
|
for (const childNote of childSearchResults.slice(0, Math.floor(limit / 2))) {
|
|
if (!processedNoteIds.has(childNote.noteId)) {
|
|
sources.push(this.createNoteSource(childNote, 0.8));
|
|
processedNoteIds.add(childNote.noteId);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
log.info(`Child search failed, falling back to direct children: ${error}`);
|
|
// Fallback to direct child enumeration
|
|
const childNotes = contextNote.getChildNotes();
|
|
for (const child of childNotes.slice(0, Math.floor(limit / 2))) {
|
|
if (!processedNoteIds.has(child.noteId) && !child.isDeleted) {
|
|
sources.push(this.createNoteSource(child, 0.8));
|
|
processedNoteIds.add(child.noteId);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get related notes (through relations)
|
|
const relatedNotes = this.getRelatedNotes(contextNote);
|
|
for (const related of relatedNotes.slice(0, Math.floor(limit / 2))) {
|
|
if (!processedNoteIds.has(related.noteId) && !related.isDeleted) {
|
|
sources.push(this.createNoteSource(related, 0.6));
|
|
processedNoteIds.add(related.noteId);
|
|
}
|
|
}
|
|
|
|
// Fill remaining slots with broader search if needed
|
|
if (sources.length < limit) {
|
|
try {
|
|
const remainingSlots = limit - sources.length;
|
|
const broadSearchResults = searchService.searchNotes(searchQuery, {
|
|
includeArchivedNotes: false,
|
|
limit: remainingSlots * 2 // Get more to filter out duplicates
|
|
});
|
|
|
|
for (const note of broadSearchResults.slice(0, remainingSlots)) {
|
|
if (!processedNoteIds.has(note.noteId)) {
|
|
sources.push(this.createNoteSource(note, 0.5));
|
|
processedNoteIds.add(note.noteId);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
log.error(`Broad search failed: ${error}`);
|
|
}
|
|
}
|
|
|
|
return sources.slice(0, limit);
|
|
}
|
|
|
|
/**
|
|
* Find notes by search across all notes using TriliumNext's search service
|
|
*/
|
|
private static findNotesBySearch(searchQuery: string, limit: number): NoteSource[] {
|
|
try {
|
|
log.info(`Performing global search for: "${searchQuery}"`);
|
|
|
|
// Use TriliumNext's search service for powerful note discovery
|
|
const searchResults = searchService.searchNotes(searchQuery, {
|
|
includeArchivedNotes: false,
|
|
fastSearch: false // Use full search for better results
|
|
});
|
|
|
|
log.info(`Global search found ${searchResults.length} notes`);
|
|
|
|
// Convert search results to NoteSource format
|
|
const sources: NoteSource[] = [];
|
|
const limitedResults = searchResults.slice(0, limit);
|
|
|
|
for (let i = 0; i < limitedResults.length; i++) {
|
|
const note = limitedResults[i];
|
|
// Calculate similarity score based on position (first results are more relevant)
|
|
const similarity = Math.max(0.1, 1.0 - (i / limitedResults.length) * 0.8);
|
|
sources.push(this.createNoteSource(note, similarity));
|
|
}
|
|
|
|
return sources;
|
|
} catch (error) {
|
|
log.error(`Error in global search: ${error}`);
|
|
// Fallback to empty results rather than crashing
|
|
return [];
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Get notes related through attributes/relations
|
|
*/
|
|
private static getRelatedNotes(note: any): any[] {
|
|
const relatedNotes: any[] = [];
|
|
|
|
// Get notes this note points to via relations
|
|
const outgoingRelations = note.getOwnedAttributes().filter((attr: any) => attr.type === 'relation');
|
|
for (const relation of outgoingRelations) {
|
|
const targetNote = becca.notes[relation.value];
|
|
if (targetNote && !targetNote.isDeleted) {
|
|
relatedNotes.push(targetNote);
|
|
}
|
|
}
|
|
|
|
// Get notes that point to this note via relations
|
|
const incomingRelations = note.getTargetRelations();
|
|
for (const relation of incomingRelations) {
|
|
const sourceNote = relation.getNote();
|
|
if (sourceNote && !sourceNote.isDeleted) {
|
|
relatedNotes.push(sourceNote);
|
|
}
|
|
}
|
|
|
|
return relatedNotes;
|
|
}
|
|
|
|
/**
|
|
* Create a NoteSource object from a note
|
|
*/
|
|
private static createNoteSource(note: any, similarity: number): NoteSource {
|
|
let noteContent: string | undefined = undefined;
|
|
if (note.type === 'text') {
|
|
const content = note.getContent();
|
|
// Handle both string and Buffer types
|
|
noteContent = typeof content === 'string' ? content :
|
|
content instanceof Buffer ? content.toString('utf8') : undefined;
|
|
}
|
|
|
|
return {
|
|
noteId: note.noteId,
|
|
title: note.title,
|
|
content: noteContent,
|
|
similarity: similarity,
|
|
branchId: note.getBranches()[0]?.branchId
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Process enhanced context using the context service
|
|
* @param query Query to process
|
|
* @param contextNoteId Optional note ID for context
|
|
* @param showThinking Whether to show thinking process
|
|
*/
|
|
static async processEnhancedContext(query: string, llmService: any, options: {
|
|
contextNoteId?: string,
|
|
showThinking?: boolean
|
|
}) {
|
|
// Use the Trilium-specific approach
|
|
const contextNoteId = options.contextNoteId || null;
|
|
const showThinking = options.showThinking || false;
|
|
|
|
// Log that we're calling contextService with the parameters
|
|
log.info(`Using enhanced context with: noteId=${contextNoteId}, showThinking=${showThinking}`);
|
|
|
|
// Call context service for processing
|
|
const results = await contextService.processQuery(
|
|
query,
|
|
llmService,
|
|
{
|
|
contextNoteId,
|
|
showThinking
|
|
}
|
|
);
|
|
|
|
// Return the generated context and sources
|
|
return {
|
|
context: results.context,
|
|
sources: results.sources.map(source => ({
|
|
noteId: source.noteId,
|
|
title: source.title,
|
|
content: source.content || undefined, // Convert null to undefined
|
|
similarity: source.similarity
|
|
}))
|
|
};
|
|
}
|
|
} |