mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-09-26 23:11:34 +08:00
update agent tools
This commit is contained in:
parent
5b81252959
commit
0d4b6a71fc
@ -1836,7 +1836,6 @@
|
|||||||
"note_chat": "Note Chat",
|
"note_chat": "Note Chat",
|
||||||
"notes_indexed": "{{ count }} note indexed",
|
"notes_indexed": "{{ count }} note indexed",
|
||||||
"notes_indexed_plural": "{{ count }} notes indexed",
|
"notes_indexed_plural": "{{ count }} notes indexed",
|
||||||
"processing": "Processing",
|
|
||||||
"reset_embeddings": "Reset Embeddings",
|
"reset_embeddings": "Reset Embeddings",
|
||||||
"show_thinking": "Show Thinking",
|
"show_thinking": "Show Thinking",
|
||||||
"show_thinking_description": "Reveals the reasoning process used to generate responses",
|
"show_thinking_description": "Reveals the reasoning process used to generate responses",
|
||||||
|
@ -49,75 +49,89 @@ export class ContextualThinkingTool {
|
|||||||
private processes: Record<string, ThinkingProcess> = {};
|
private processes: Record<string, ThinkingProcess> = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a new thinking process for a given query
|
* Start a new thinking process for a query
|
||||||
*
|
*
|
||||||
* @param query The user query that initiated the thinking process
|
* @param query The user's query
|
||||||
* @returns The ID of the new thinking process
|
* @returns The created thinking process ID
|
||||||
*/
|
*/
|
||||||
startThinking(query: string): string {
|
startThinking(query: string): string {
|
||||||
const id = this.generateProcessId();
|
const thinkingId = `thinking_${Date.now()}_${ContextualThinkingTool.thinkingCounter++}`;
|
||||||
|
|
||||||
this.processes[id] = {
|
log.info(`Starting thinking process: ${thinkingId} for query "${query.substring(0, 50)}..."`);
|
||||||
id,
|
|
||||||
|
this.processes[thinkingId] = {
|
||||||
|
id: thinkingId,
|
||||||
query,
|
query,
|
||||||
steps: [],
|
steps: [],
|
||||||
status: 'in_progress',
|
status: 'in_progress',
|
||||||
startTime: Date.now()
|
startTime: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
this.activeProcId = id;
|
// Set as active process
|
||||||
return id;
|
this.activeProcId = thinkingId;
|
||||||
|
|
||||||
|
// Initialize with some starter thinking steps
|
||||||
|
this.addThinkingStep(thinkingId, {
|
||||||
|
type: 'observation',
|
||||||
|
content: `Starting analysis of the query: "${query}"`
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addThinkingStep(thinkingId, {
|
||||||
|
type: 'question',
|
||||||
|
content: `What are the key components of this query that need to be addressed?`
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addThinkingStep(thinkingId, {
|
||||||
|
type: 'observation',
|
||||||
|
content: `Breaking down the query to understand its requirements and context.`
|
||||||
|
});
|
||||||
|
|
||||||
|
return thinkingId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a thinking step to the current active process
|
* Add a thinking step to a process
|
||||||
*
|
*
|
||||||
* @param content The content of the thinking step
|
* @param processId The ID of the process to add to
|
||||||
* @param type The type of thinking step
|
* @param step The thinking step to add
|
||||||
* @param options Additional options for the step
|
* @returns The ID of the added step
|
||||||
* @returns The ID of the new step
|
|
||||||
*/
|
*/
|
||||||
addThinkingStep(
|
addThinkingStep(
|
||||||
content: string,
|
processId: string,
|
||||||
type: ThinkingStep['type'],
|
step: Omit<ThinkingStep, 'id'>,
|
||||||
options: {
|
parentId?: string
|
||||||
confidence?: number;
|
): string {
|
||||||
sources?: string[];
|
const process = this.processes[processId];
|
||||||
parentId?: string;
|
|
||||||
metadata?: Record<string, any>;
|
if (!process) {
|
||||||
} = {}
|
throw new Error(`Thinking process ${processId} not found`);
|
||||||
): string | null {
|
|
||||||
if (!this.activeProcId || !this.processes[this.activeProcId]) {
|
|
||||||
log.error("No active thinking process to add step to");
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const stepId = this.generateStepId();
|
// Create full step with ID
|
||||||
const step: ThinkingStep = {
|
const fullStep: ThinkingStep = {
|
||||||
id: stepId,
|
id: `step_${Date.now()}_${Math.floor(Math.random() * 10000)}`,
|
||||||
content,
|
...step,
|
||||||
type,
|
parentId
|
||||||
...options
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add to parent's children if a parent is specified
|
// Add to process steps
|
||||||
if (options.parentId) {
|
process.steps.push(fullStep);
|
||||||
const parentIdx = this.processes[this.activeProcId].steps.findIndex(
|
|
||||||
s => s.id === options.parentId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (parentIdx >= 0) {
|
// If this step has a parent, update the parent's children list
|
||||||
const parent = this.processes[this.activeProcId].steps[parentIdx];
|
if (parentId) {
|
||||||
if (!parent.children) {
|
const parentStep = process.steps.find(s => s.id === parentId);
|
||||||
parent.children = [];
|
if (parentStep) {
|
||||||
|
if (!parentStep.children) {
|
||||||
|
parentStep.children = [];
|
||||||
}
|
}
|
||||||
parent.children.push(stepId);
|
parentStep.children.push(fullStep.id);
|
||||||
this.processes[this.activeProcId].steps[parentIdx] = parent;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.processes[this.activeProcId].steps.push(step);
|
// Log the step addition with more detail
|
||||||
return stepId;
|
log.info(`Added thinking step to process ${processId}: [${step.type}] ${step.content.substring(0, 100)}...`);
|
||||||
|
|
||||||
|
return fullStep.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -177,25 +191,71 @@ export class ContextualThinkingTool {
|
|||||||
log.info(`Found thinking process with ${process.steps.length} steps for query: "${process.query.substring(0, 50)}..."`);
|
log.info(`Found thinking process with ${process.steps.length} steps for query: "${process.query.substring(0, 50)}..."`);
|
||||||
|
|
||||||
let html = "<div class='thinking-process'>";
|
let html = "<div class='thinking-process'>";
|
||||||
html += `<h4>Thinking Process</h4>`;
|
html += `<h4>Reasoning Process</h4>`;
|
||||||
|
html += `<div class='thinking-query'>${process.query}</div>`;
|
||||||
|
|
||||||
for (const step of process.steps) {
|
// Show overall time taken for the thinking process
|
||||||
html += `<div class='thinking-step ${step.type || ""}'>`;
|
const duration = process.endTime ?
|
||||||
|
Math.round((process.endTime - process.startTime) / 1000) :
|
||||||
|
Math.round((Date.now() - process.startTime) / 1000);
|
||||||
|
|
||||||
|
html += `<div class='thinking-meta'>Analysis took ${duration} seconds</div>`;
|
||||||
|
|
||||||
|
// Create a more structured visualization with indentation for parent-child relationships
|
||||||
|
const renderStep = (step: ThinkingStep, level: number = 0) => {
|
||||||
|
const indent = level * 20; // 20px indentation per level
|
||||||
|
|
||||||
|
let stepHtml = `<div class='thinking-step ${step.type || ""}' style='margin-left: ${indent}px'>`;
|
||||||
|
|
||||||
// Add an icon based on step type
|
// Add an icon based on step type
|
||||||
const icon = this.getStepIcon(step.type);
|
const icon = this.getStepIcon(step.type);
|
||||||
html += `<span class='bx ${icon}'></span> `;
|
stepHtml += `<span class='bx ${icon}'></span> `;
|
||||||
|
|
||||||
// Add the step content
|
// Add the step content
|
||||||
html += step.content;
|
stepHtml += step.content;
|
||||||
|
|
||||||
// Show confidence if available
|
// Show confidence if available
|
||||||
if (step.metadata?.confidence) {
|
if (step.metadata?.confidence) {
|
||||||
const confidence = Math.round((step.metadata.confidence as number) * 100);
|
const confidence = Math.round((step.metadata.confidence as number) * 100);
|
||||||
html += ` <span class='thinking-confidence'>(Confidence: ${confidence}%)</span>`;
|
stepHtml += ` <span class='thinking-confidence'>(Confidence: ${confidence}%)</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
html += `</div>`;
|
// Show sources if available
|
||||||
|
if (step.sources && step.sources.length > 0) {
|
||||||
|
stepHtml += `<div class='thinking-sources'>Sources: ${step.sources.join(', ')}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
stepHtml += `</div>`;
|
||||||
|
|
||||||
|
return stepHtml;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to render a step and all its children recursively
|
||||||
|
const renderStepWithChildren = (stepId: string, level: number = 0) => {
|
||||||
|
const step = process.steps.find(s => s.id === stepId);
|
||||||
|
if (!step) return '';
|
||||||
|
|
||||||
|
let html = renderStep(step, level);
|
||||||
|
|
||||||
|
if (step.children && step.children.length > 0) {
|
||||||
|
for (const childId of step.children) {
|
||||||
|
html += renderStepWithChildren(childId, level + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render top-level steps and their children
|
||||||
|
const topLevelSteps = process.steps.filter(s => !s.parentId);
|
||||||
|
for (const step of topLevelSteps) {
|
||||||
|
html += renderStep(step);
|
||||||
|
|
||||||
|
if (step.children && step.children.length > 0) {
|
||||||
|
for (const childId of step.children) {
|
||||||
|
html += renderStepWithChildren(childId, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html += "</div>";
|
html += "</div>";
|
||||||
@ -232,7 +292,61 @@ export class ContextualThinkingTool {
|
|||||||
return "No thinking process available.";
|
return "No thinking process available.";
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.visualizeThinking(thinkingId);
|
let summary = `## Reasoning Process for Query: "${process.query}"\n\n`;
|
||||||
|
|
||||||
|
// Group steps by type for better organization
|
||||||
|
const observations = process.steps.filter(s => s.type === 'observation');
|
||||||
|
const questions = process.steps.filter(s => s.type === 'question');
|
||||||
|
const hypotheses = process.steps.filter(s => s.type === 'hypothesis');
|
||||||
|
const evidence = process.steps.filter(s => s.type === 'evidence');
|
||||||
|
const conclusions = process.steps.filter(s => s.type === 'conclusion');
|
||||||
|
|
||||||
|
// Add observations
|
||||||
|
if (observations.length > 0) {
|
||||||
|
summary += "### Observations:\n";
|
||||||
|
observations.forEach(step => {
|
||||||
|
summary += `- ${step.content}\n`;
|
||||||
|
});
|
||||||
|
summary += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add questions
|
||||||
|
if (questions.length > 0) {
|
||||||
|
summary += "### Questions Considered:\n";
|
||||||
|
questions.forEach(step => {
|
||||||
|
summary += `- ${step.content}\n`;
|
||||||
|
});
|
||||||
|
summary += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add hypotheses
|
||||||
|
if (hypotheses.length > 0) {
|
||||||
|
summary += "### Hypotheses:\n";
|
||||||
|
hypotheses.forEach(step => {
|
||||||
|
summary += `- ${step.content}\n`;
|
||||||
|
});
|
||||||
|
summary += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add evidence
|
||||||
|
if (evidence.length > 0) {
|
||||||
|
summary += "### Evidence Gathered:\n";
|
||||||
|
evidence.forEach(step => {
|
||||||
|
summary += `- ${step.content}\n`;
|
||||||
|
});
|
||||||
|
summary += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add conclusions
|
||||||
|
if (conclusions.length > 0) {
|
||||||
|
summary += "### Conclusions:\n";
|
||||||
|
conclusions.forEach(step => {
|
||||||
|
summary += `- ${step.content}\n`;
|
||||||
|
});
|
||||||
|
summary += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -41,6 +41,15 @@ export interface NoteHierarchyLevel {
|
|||||||
children?: NoteHierarchyLevel[];
|
children?: NoteHierarchyLevel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NoteStructure {
|
||||||
|
noteId: string;
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
childCount: number;
|
||||||
|
attributes: Array<{name: string, value: string}>;
|
||||||
|
parentPath: Array<{title: string, noteId: string}>;
|
||||||
|
}
|
||||||
|
|
||||||
export class NoteNavigatorTool {
|
export class NoteNavigatorTool {
|
||||||
private maxPathLength: number = 20;
|
private maxPathLength: number = 20;
|
||||||
private maxBreadth: number = 100;
|
private maxBreadth: number = 100;
|
||||||
@ -113,45 +122,6 @@ export class NoteNavigatorTool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the parent notes of a given note
|
|
||||||
*/
|
|
||||||
getParentNotes(noteId: string): NoteInfo[] {
|
|
||||||
try {
|
|
||||||
const note = becca.notes[noteId];
|
|
||||||
if (!note || !note.parents) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return note.parents
|
|
||||||
.map(parent => this.getNoteInfo(parent.noteId))
|
|
||||||
.filter((info): info is NoteInfo => info !== null);
|
|
||||||
} catch (error: any) {
|
|
||||||
log.error(`Error getting parent notes: ${error.message}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the children notes of a given note
|
|
||||||
*/
|
|
||||||
getChildNotes(noteId: string, maxChildren: number = this.maxBreadth): NoteInfo[] {
|
|
||||||
try {
|
|
||||||
const note = becca.notes[noteId];
|
|
||||||
if (!note || !note.children) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return note.children
|
|
||||||
.slice(0, maxChildren)
|
|
||||||
.map(child => this.getNoteInfo(child.noteId))
|
|
||||||
.filter((info): info is NoteInfo => info !== null);
|
|
||||||
} catch (error: any) {
|
|
||||||
log.error(`Error getting child notes: ${error.message}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a note's hierarchy (children up to specified depth)
|
* Get a note's hierarchy (children up to specified depth)
|
||||||
* This is useful for the LLM to understand the structure within a note's subtree
|
* This is useful for the LLM to understand the structure within a note's subtree
|
||||||
@ -349,7 +319,7 @@ export class NoteNavigatorTool {
|
|||||||
/**
|
/**
|
||||||
* Get clones of a note (if any)
|
* Get clones of a note (if any)
|
||||||
*/
|
*/
|
||||||
getNoteClones(noteId: string): NoteInfo[] {
|
async getNoteClones(noteId: string): Promise<NoteInfo[]> {
|
||||||
try {
|
try {
|
||||||
const note = becca.notes[noteId];
|
const note = becca.notes[noteId];
|
||||||
if (!note) {
|
if (!note) {
|
||||||
@ -362,7 +332,10 @@ export class NoteNavigatorTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return parent notes, which represent different contexts for this note
|
// Return parent notes, which represent different contexts for this note
|
||||||
return this.getParentNotes(noteId);
|
const parents = await this.getParentNotes(noteId);
|
||||||
|
return parents
|
||||||
|
.map(parent => this.getNoteInfo(parent.noteId))
|
||||||
|
.filter((info): info is NoteInfo => info !== null);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
log.error(`Error getting note clones: ${error.message}`);
|
log.error(`Error getting note clones: ${error.message}`);
|
||||||
return [];
|
return [];
|
||||||
@ -373,7 +346,7 @@ export class NoteNavigatorTool {
|
|||||||
* Generate a readable overview of a note's position in the hierarchy
|
* Generate a readable overview of a note's position in the hierarchy
|
||||||
* This is useful for the LLM to understand the context of a note
|
* This is useful for the LLM to understand the context of a note
|
||||||
*/
|
*/
|
||||||
getNoteContextDescription(noteId: string): string {
|
async getNoteContextDescription(noteId: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const note = becca.notes[noteId];
|
const note = becca.notes[noteId];
|
||||||
if (!note) {
|
if (!note) {
|
||||||
@ -409,8 +382,9 @@ export class NoteNavigatorTool {
|
|||||||
result += `Path: ${path.notePathTitles.join(' > ')}\n`;
|
result += `Path: ${path.notePathTitles.join(' > ')}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Children info
|
// Children info using the async function
|
||||||
const children = this.getChildNotes(noteId, 5);
|
const children = await this.getChildNotes(noteId, 5);
|
||||||
|
|
||||||
if (children.length > 0) {
|
if (children.length > 0) {
|
||||||
result += `\nContains ${note.children.length} child notes`;
|
result += `\nContains ${note.children.length} child notes`;
|
||||||
if (children.length < note.children.length) {
|
if (children.length < note.children.length) {
|
||||||
@ -419,7 +393,7 @@ export class NoteNavigatorTool {
|
|||||||
result += `:\n`;
|
result += `:\n`;
|
||||||
|
|
||||||
for (const child of children) {
|
for (const child of children) {
|
||||||
result += `- ${child.title} (${child.type})\n`;
|
result += `- ${child.title}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (children.length < note.children.length) {
|
if (children.length < note.children.length) {
|
||||||
@ -458,6 +432,185 @@ export class NoteNavigatorTool {
|
|||||||
return "Error generating note context description.";
|
return "Error generating note context description.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the structure of a note including its hierarchy and attributes
|
||||||
|
*
|
||||||
|
* @param noteId The ID of the note to get structure for
|
||||||
|
* @returns Structure information about the note
|
||||||
|
*/
|
||||||
|
async getNoteStructure(noteId: string): Promise<NoteStructure> {
|
||||||
|
try {
|
||||||
|
log.info(`Getting note structure for note ${noteId}`);
|
||||||
|
|
||||||
|
// Get the note from becca
|
||||||
|
const note = becca.notes[noteId];
|
||||||
|
|
||||||
|
if (!note) {
|
||||||
|
throw new Error(`Note ${noteId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get child notes count
|
||||||
|
const childCount = note.children.length;
|
||||||
|
|
||||||
|
// Get attributes
|
||||||
|
const attributes = note.getAttributes().map(attr => ({
|
||||||
|
name: attr.name,
|
||||||
|
value: attr.value
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Build parent path
|
||||||
|
const parentPath: Array<{title: string, noteId: string}> = [];
|
||||||
|
let current = note.parents[0]; // Get first parent
|
||||||
|
|
||||||
|
while (current && current.noteId !== 'root') {
|
||||||
|
parentPath.unshift({
|
||||||
|
title: current.title,
|
||||||
|
noteId: current.noteId
|
||||||
|
});
|
||||||
|
|
||||||
|
current = current.parents[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
noteId: note.noteId,
|
||||||
|
title: note.title,
|
||||||
|
type: note.type,
|
||||||
|
childCount,
|
||||||
|
attributes,
|
||||||
|
parentPath
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Error getting note structure: ${error}`);
|
||||||
|
// Return a minimal structure with empty arrays to avoid null errors
|
||||||
|
return {
|
||||||
|
noteId,
|
||||||
|
title: 'Unknown',
|
||||||
|
type: 'unknown',
|
||||||
|
childCount: 0,
|
||||||
|
attributes: [],
|
||||||
|
parentPath: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get child notes of a specified note
|
||||||
|
*/
|
||||||
|
async getChildNotes(noteId: string, limit: number = 10): Promise<Array<{noteId: string, title: string}>> {
|
||||||
|
try {
|
||||||
|
const note = becca.notes[noteId];
|
||||||
|
|
||||||
|
if (!note) {
|
||||||
|
throw new Error(`Note ${noteId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return note.children
|
||||||
|
.slice(0, limit)
|
||||||
|
.map(child => ({
|
||||||
|
noteId: child.noteId,
|
||||||
|
title: child.title
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Error getting child notes: ${error}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get parent notes of a specified note
|
||||||
|
*/
|
||||||
|
async getParentNotes(noteId: string): Promise<Array<{noteId: string, title: string}>> {
|
||||||
|
try {
|
||||||
|
const note = becca.notes[noteId];
|
||||||
|
|
||||||
|
if (!note) {
|
||||||
|
throw new Error(`Note ${noteId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return note.parents.map(parent => ({
|
||||||
|
noteId: parent.noteId,
|
||||||
|
title: parent.title
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Error getting parent notes: ${error}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find notes linked to/from the specified note
|
||||||
|
*/
|
||||||
|
async getLinkedNotes(noteId: string, limit: number = 10): Promise<Array<{noteId: string, title: string, direction: 'from'|'to'}>> {
|
||||||
|
try {
|
||||||
|
const note = becca.notes[noteId];
|
||||||
|
|
||||||
|
if (!note) {
|
||||||
|
throw new Error(`Note ${noteId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Links from this note to others
|
||||||
|
const outboundLinks = note.getRelations()
|
||||||
|
.slice(0, Math.floor(limit / 2))
|
||||||
|
.map(relation => ({
|
||||||
|
noteId: relation.targetNoteId || '', // Ensure noteId is never undefined
|
||||||
|
title: relation.name,
|
||||||
|
direction: 'to' as const
|
||||||
|
}))
|
||||||
|
.filter(link => link.noteId !== ''); // Filter out any with empty noteId
|
||||||
|
|
||||||
|
// Links from other notes to this one
|
||||||
|
const inboundLinks: Array<{noteId: string, title: string, direction: 'from'}> = [];
|
||||||
|
|
||||||
|
// Find all notes that have relations pointing to this note
|
||||||
|
for (const relatedNoteId in becca.notes) {
|
||||||
|
const relatedNote = becca.notes[relatedNoteId];
|
||||||
|
if (relatedNote && !relatedNote.isDeleted) {
|
||||||
|
const relations = relatedNote.getRelations();
|
||||||
|
for (const relation of relations) {
|
||||||
|
if (relation.targetNoteId === noteId) {
|
||||||
|
inboundLinks.push({
|
||||||
|
noteId: relatedNote.noteId,
|
||||||
|
title: relation.name,
|
||||||
|
direction: 'from'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Break if we've found enough inbound links
|
||||||
|
if (inboundLinks.length >= Math.floor(limit / 2)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Break if we've found enough inbound links
|
||||||
|
if (inboundLinks.length >= Math.floor(limit / 2)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...outboundLinks, ...inboundLinks.slice(0, Math.floor(limit / 2))];
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Error getting linked notes: ${error}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the full path of a note from root
|
||||||
|
*/
|
||||||
|
async getNotePath(noteId: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const structure = await this.getNoteStructure(noteId);
|
||||||
|
const path = structure.parentPath.map(p => p.title);
|
||||||
|
path.push(structure.title);
|
||||||
|
|
||||||
|
return path.join(' > ');
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Error getting note path: ${error}`);
|
||||||
|
return 'Unknown path';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NoteNavigatorTool;
|
export default NoteNavigatorTool;
|
||||||
|
@ -41,11 +41,17 @@ export class QueryDecompositionTool {
|
|||||||
*/
|
*/
|
||||||
decomposeQuery(query: string, context?: string): DecomposedQuery {
|
decomposeQuery(query: string, context?: string): DecomposedQuery {
|
||||||
try {
|
try {
|
||||||
|
// Log the decomposition attempt for tracking
|
||||||
|
log.info(`Decomposing query: "${query.substring(0, 100)}..."`);
|
||||||
|
|
||||||
// Assess query complexity to determine if decomposition is needed
|
// Assess query complexity to determine if decomposition is needed
|
||||||
const complexity = this.assessQueryComplexity(query);
|
const complexity = this.assessQueryComplexity(query);
|
||||||
|
log.info(`Query complexity assessment: ${complexity}/10`);
|
||||||
|
|
||||||
// For simple queries, just return the original as a single sub-query
|
// For simple queries, just return the original as a single sub-query
|
||||||
if (complexity < 3) {
|
// Use a lower threshold (2 instead of 3) to decompose more queries
|
||||||
|
if (complexity < 2) {
|
||||||
|
log.info(`Query is simple (complexity ${complexity}), returning as single sub-query`);
|
||||||
return {
|
return {
|
||||||
originalQuery: query,
|
originalQuery: query,
|
||||||
subQueries: [{
|
subQueries: [{
|
||||||
@ -61,6 +67,12 @@ export class QueryDecompositionTool {
|
|||||||
|
|
||||||
// For complex queries, perform decomposition
|
// For complex queries, perform decomposition
|
||||||
const subQueries = this.createSubQueries(query, context);
|
const subQueries = this.createSubQueries(query, context);
|
||||||
|
log.info(`Decomposed query into ${subQueries.length} sub-queries`);
|
||||||
|
|
||||||
|
// Log the sub-queries for better visibility
|
||||||
|
subQueries.forEach((sq, index) => {
|
||||||
|
log.info(`Sub-query ${index + 1}: "${sq.text}" - Reason: ${sq.reason}`);
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
originalQuery: query,
|
originalQuery: query,
|
||||||
@ -248,9 +260,18 @@ export class QueryDecompositionTool {
|
|||||||
private createSubQueries(query: string, context?: string): SubQuery[] {
|
private createSubQueries(query: string, context?: string): SubQuery[] {
|
||||||
const subQueries: SubQuery[] = [];
|
const subQueries: SubQuery[] = [];
|
||||||
|
|
||||||
// Simple heuristics for breaking down the query
|
// Use context to enhance sub-query generation if available
|
||||||
// In a real implementation, this would be much more sophisticated,
|
if (context) {
|
||||||
// using natural language understanding to identify different intents
|
log.info(`Using context to enhance sub-query generation`);
|
||||||
|
|
||||||
|
// Add context-specific questions
|
||||||
|
subQueries.push({
|
||||||
|
id: this.generateSubQueryId(),
|
||||||
|
text: `What key information in the current note relates to: "${query}"?`,
|
||||||
|
reason: 'Identifying directly relevant information in the current context',
|
||||||
|
isAnswered: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Look for multiple question marks
|
// 1. Look for multiple question marks
|
||||||
const questionSplit = query.split(/\?/).filter(q => q.trim().length > 0);
|
const questionSplit = query.split(/\?/).filter(q => q.trim().length > 0);
|
||||||
@ -266,6 +287,15 @@ export class QueryDecompositionTool {
|
|||||||
isAnswered: false
|
isAnswered: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also add a synthesis question
|
||||||
|
subQueries.push({
|
||||||
|
id: this.generateSubQueryId(),
|
||||||
|
text: `How do the answers to these questions relate to each other in the context of the original query?`,
|
||||||
|
reason: 'Synthesizing information from multiple questions',
|
||||||
|
isAnswered: false
|
||||||
|
});
|
||||||
|
|
||||||
return subQueries;
|
return subQueries;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,8 +335,22 @@ export class QueryDecompositionTool {
|
|||||||
|
|
||||||
subQueries.push({
|
subQueries.push({
|
||||||
id: this.generateSubQueryId(),
|
id: this.generateSubQueryId(),
|
||||||
text: `What are the main differences and similarities between ${item1} and ${item2}?`,
|
text: `What are the main differences between ${item1} and ${item2}?`,
|
||||||
reason: 'Direct comparison after understanding each item',
|
reason: 'Understanding key differences',
|
||||||
|
isAnswered: false
|
||||||
|
});
|
||||||
|
|
||||||
|
subQueries.push({
|
||||||
|
id: this.generateSubQueryId(),
|
||||||
|
text: `What are the main similarities between ${item1} and ${item2}?`,
|
||||||
|
reason: 'Understanding key similarities',
|
||||||
|
isAnswered: false
|
||||||
|
});
|
||||||
|
|
||||||
|
subQueries.push({
|
||||||
|
id: this.generateSubQueryId(),
|
||||||
|
text: `What practical implications do these differences and similarities have?`,
|
||||||
|
reason: 'Understanding practical significance of the comparison',
|
||||||
isAnswered: false
|
isAnswered: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -317,7 +361,8 @@ export class QueryDecompositionTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. For complex questions without clear separation, create topic-based sub-queries
|
// 3. For complex questions without clear separation, create topic-based sub-queries
|
||||||
if (query.length > 100) {
|
// Lowered the threshold to process more queries this way
|
||||||
|
if (query.length > 50) {
|
||||||
// Extract potential key topics from the query
|
// Extract potential key topics from the query
|
||||||
const words = query.toLowerCase().split(/\W+/).filter(w =>
|
const words = query.toLowerCase().split(/\W+/).filter(w =>
|
||||||
w.length > 3 &&
|
w.length > 3 &&
|
||||||
@ -333,7 +378,7 @@ export class QueryDecompositionTool {
|
|||||||
// Get top frequent words
|
// Get top frequent words
|
||||||
const topWords = Object.entries(wordFrequency)
|
const topWords = Object.entries(wordFrequency)
|
||||||
.sort((a, b) => b[1] - a[1])
|
.sort((a, b) => b[1] - a[1])
|
||||||
.slice(0, 3)
|
.slice(0, 4) // Increased from 3 to 4
|
||||||
.map(entry => entry[0]);
|
.map(entry => entry[0]);
|
||||||
|
|
||||||
if (topWords.length > 0) {
|
if (topWords.length > 0) {
|
||||||
@ -345,16 +390,38 @@ export class QueryDecompositionTool {
|
|||||||
isAnswered: false
|
isAnswered: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create relationship sub-query if multiple top words
|
// Add individual queries for each key topic
|
||||||
if (topWords.length > 1) {
|
topWords.forEach(word => {
|
||||||
subQueries.push({
|
subQueries.push({
|
||||||
id: this.generateSubQueryId(),
|
id: this.generateSubQueryId(),
|
||||||
text: `How do ${topWords.join(' and ')} relate to each other?`,
|
text: `What specific details about "${word}" are most relevant to the query?`,
|
||||||
reason: 'Understanding relationships between key topics',
|
reason: `Detailed exploration of the "${word}" concept`,
|
||||||
isAnswered: false
|
isAnswered: false
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create relationship sub-query if multiple top words
|
||||||
|
if (topWords.length > 1) {
|
||||||
|
for (let i = 0; i < topWords.length; i++) {
|
||||||
|
for (let j = i + 1; j < topWords.length; j++) {
|
||||||
|
subQueries.push({
|
||||||
|
id: this.generateSubQueryId(),
|
||||||
|
text: `How do ${topWords[i]} and ${topWords[j]} relate to each other?`,
|
||||||
|
reason: `Understanding relationship between ${topWords[i]} and ${topWords[j]}`,
|
||||||
|
isAnswered: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a "what else" query to ensure comprehensive coverage
|
||||||
|
subQueries.push({
|
||||||
|
id: this.generateSubQueryId(),
|
||||||
|
text: `What other important aspects should be considered about this topic that might not be immediately obvious?`,
|
||||||
|
reason: 'Exploring non-obvious but relevant information',
|
||||||
|
isAnswered: false
|
||||||
|
});
|
||||||
|
|
||||||
// Add the original query as the final synthesizing question
|
// Add the original query as the final synthesizing question
|
||||||
subQueries.push({
|
subQueries.push({
|
||||||
id: this.generateSubQueryId(),
|
id: this.generateSubQueryId(),
|
||||||
@ -368,10 +435,26 @@ export class QueryDecompositionTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: If we can't meaningfully decompose, just use the original query
|
// Fallback: If we can't meaningfully decompose, just use the original query
|
||||||
|
// But also add some generic exploration questions
|
||||||
subQueries.push({
|
subQueries.push({
|
||||||
id: this.generateSubQueryId(),
|
id: this.generateSubQueryId(),
|
||||||
text: query,
|
text: query,
|
||||||
reason: 'Question treated as a single unit',
|
reason: 'Primary question',
|
||||||
|
isAnswered: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add generic exploration questions even for "simple" queries
|
||||||
|
subQueries.push({
|
||||||
|
id: this.generateSubQueryId(),
|
||||||
|
text: `What background information is helpful to understand this query better?`,
|
||||||
|
reason: 'Gathering background context',
|
||||||
|
isAnswered: false
|
||||||
|
});
|
||||||
|
|
||||||
|
subQueries.push({
|
||||||
|
id: this.generateSubQueryId(),
|
||||||
|
text: `What related concepts might be important to consider?`,
|
||||||
|
reason: 'Exploring related concepts',
|
||||||
isAnswered: false
|
isAnswered: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import log from '../../log.js';
|
import log from '../../log.js';
|
||||||
|
import type { ContextService } from '../context/modules/context_service.js';
|
||||||
|
|
||||||
// Define interface for context service to avoid circular imports
|
// Define interface for context service to avoid circular imports
|
||||||
interface IContextService {
|
interface IContextService {
|
||||||
@ -48,6 +49,12 @@ export interface ChunkSearchResultItem {
|
|||||||
parentId?: string;
|
parentId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VectorSearchOptions {
|
||||||
|
limit?: number;
|
||||||
|
threshold?: number;
|
||||||
|
includeContent?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export class VectorSearchTool {
|
export class VectorSearchTool {
|
||||||
private contextService: IContextService | null = null;
|
private contextService: IContextService | null = null;
|
||||||
private maxResults: number = 5;
|
private maxResults: number = 5;
|
||||||
@ -64,6 +71,77 @@ export class VectorSearchTool {
|
|||||||
log.info('Context service set in VectorSearchTool');
|
log.info('Context service set in VectorSearchTool');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a vector search for related notes
|
||||||
|
*/
|
||||||
|
async search(
|
||||||
|
query: string,
|
||||||
|
contextNoteId?: string,
|
||||||
|
searchOptions: VectorSearchOptions = {}
|
||||||
|
): Promise<VectorSearchResult[]> {
|
||||||
|
if (!this.contextService) {
|
||||||
|
throw new Error("Context service not set, call setContextService() first");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Set more aggressive defaults to return more content
|
||||||
|
const options = {
|
||||||
|
limit: searchOptions.limit || 15, // Increased from default (likely 5 or 10)
|
||||||
|
threshold: searchOptions.threshold || 0.5, // Lower threshold to include more results (likely 0.65 or 0.7 before)
|
||||||
|
includeContent: searchOptions.includeContent !== undefined ? searchOptions.includeContent : true,
|
||||||
|
...searchOptions
|
||||||
|
};
|
||||||
|
|
||||||
|
log.info(`Vector search: "${query.substring(0, 50)}..." with limit=${options.limit}, threshold=${options.threshold}`);
|
||||||
|
|
||||||
|
// Check if contextService is set again to satisfy TypeScript
|
||||||
|
if (!this.contextService) {
|
||||||
|
throw new Error("Context service not set, call setContextService() first");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use contextService methods instead of direct imports
|
||||||
|
const results = await this.contextService.findRelevantNotesMultiQuery(
|
||||||
|
[query],
|
||||||
|
contextNoteId || null,
|
||||||
|
options.limit
|
||||||
|
);
|
||||||
|
|
||||||
|
// Log the number of results
|
||||||
|
log.info(`Vector search found ${results.length} relevant notes`);
|
||||||
|
|
||||||
|
// Include more content from each note to provide richer context
|
||||||
|
if (options.includeContent) {
|
||||||
|
// Get full context for each note rather than summaries
|
||||||
|
for (let i = 0; i < results.length; i++) {
|
||||||
|
const result = results[i];
|
||||||
|
try {
|
||||||
|
// Use contextService instead of direct import
|
||||||
|
if (this.contextService && 'processQuery' in this.contextService) {
|
||||||
|
const contextResult = await this.contextService.processQuery(
|
||||||
|
`Provide details about the note: ${result.title}`,
|
||||||
|
null,
|
||||||
|
result.noteId,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (contextResult && contextResult.context) {
|
||||||
|
// Use more of the content, up to 2000 chars
|
||||||
|
result.content = contextResult.context.substring(0, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Error getting content for note ${result.noteId}: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Vector search error: ${error}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search for notes that are semantically related to the query
|
* Search for notes that are semantically related to the query
|
||||||
*/
|
*/
|
||||||
|
@ -414,12 +414,19 @@ export class AIServiceManager {
|
|||||||
showThinking: boolean = false,
|
showThinking: boolean = false,
|
||||||
relevantNotes: Array<any> = []
|
relevantNotes: Array<any> = []
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return contextService.getAgentToolsContext(
|
// Just use the context service directly
|
||||||
noteId,
|
try {
|
||||||
query,
|
const cs = (await import('./context/modules/context_service.js')).default;
|
||||||
showThinking,
|
return cs.getAgentToolsContext(
|
||||||
relevantNotes
|
noteId,
|
||||||
);
|
query,
|
||||||
|
showThinking,
|
||||||
|
relevantNotes
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Error in AIServiceManager.getAgentToolsContext: ${error}`);
|
||||||
|
return `Error generating enhanced context: ${error}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,15 +197,222 @@ export class ContextService {
|
|||||||
relevantNotes: Array<any> = []
|
relevantNotes: Array<any> = []
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
try {
|
try {
|
||||||
return await aiServiceManager.getInstance().getAgentToolsContext(
|
log.info(`Building enhanced agent tools context for query: "${query.substring(0, 50)}...", noteId=${noteId}, showThinking=${showThinking}`);
|
||||||
noteId,
|
|
||||||
query,
|
// Make sure agent tools are initialized
|
||||||
showThinking,
|
const agentManager = aiServiceManager.getInstance();
|
||||||
relevantNotes
|
|
||||||
);
|
// Initialize all tools if not already done
|
||||||
|
if (!agentManager.getAgentTools().isInitialized()) {
|
||||||
|
await agentManager.initializeAgentTools();
|
||||||
|
log.info("Agent tools initialized on-demand in getAgentToolsContext");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all agent tools
|
||||||
|
const vectorSearchTool = agentManager.getVectorSearchTool();
|
||||||
|
const noteNavigatorTool = agentManager.getNoteNavigatorTool();
|
||||||
|
const queryDecompositionTool = agentManager.getQueryDecompositionTool();
|
||||||
|
const contextualThinkingTool = agentManager.getContextualThinkingTool();
|
||||||
|
|
||||||
|
// Step 1: Start a thinking process
|
||||||
|
const thinkingId = contextualThinkingTool.startThinking(query);
|
||||||
|
contextualThinkingTool.addThinkingStep(thinkingId, {
|
||||||
|
type: 'observation',
|
||||||
|
content: `Analyzing query: "${query}" for note ID: ${noteId}`
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 2: Decompose the query into sub-questions
|
||||||
|
const decomposedQuery = queryDecompositionTool.decomposeQuery(query);
|
||||||
|
contextualThinkingTool.addThinkingStep(thinkingId, {
|
||||||
|
type: 'observation',
|
||||||
|
content: `Query complexity: ${decomposedQuery.complexity}/10. Decomposed into ${decomposedQuery.subQueries.length} sub-queries.`
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log each sub-query as a thinking step
|
||||||
|
for (const subQuery of decomposedQuery.subQueries) {
|
||||||
|
contextualThinkingTool.addThinkingStep(thinkingId, {
|
||||||
|
type: 'question',
|
||||||
|
content: subQuery.text,
|
||||||
|
metadata: {
|
||||||
|
reason: subQuery.reason
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Use vector search to find related content
|
||||||
|
// Use an aggressive search with lower threshold to get more results
|
||||||
|
const searchOptions = {
|
||||||
|
threshold: 0.5, // Lower threshold to include more matches
|
||||||
|
limit: 15 // Get more results
|
||||||
|
};
|
||||||
|
|
||||||
|
const vectorSearchPromises = [];
|
||||||
|
|
||||||
|
// Search for each sub-query that isn't just the original query
|
||||||
|
for (const subQuery of decomposedQuery.subQueries.filter(sq => sq.text !== query)) {
|
||||||
|
vectorSearchPromises.push(
|
||||||
|
vectorSearchTool.search(subQuery.text, noteId, searchOptions)
|
||||||
|
.then(results => {
|
||||||
|
return {
|
||||||
|
query: subQuery.text,
|
||||||
|
results
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all searches to complete
|
||||||
|
const searchResults = await Promise.all(vectorSearchPromises);
|
||||||
|
|
||||||
|
// Record the search results in thinking steps
|
||||||
|
let totalResults = 0;
|
||||||
|
for (const result of searchResults) {
|
||||||
|
totalResults += result.results.length;
|
||||||
|
|
||||||
|
if (result.results.length > 0) {
|
||||||
|
const stepId = contextualThinkingTool.addThinkingStep(thinkingId, {
|
||||||
|
type: 'evidence',
|
||||||
|
content: `Found ${result.results.length} relevant notes for sub-query: "${result.query}"`,
|
||||||
|
metadata: {
|
||||||
|
searchQuery: result.query
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add top results as children
|
||||||
|
for (const note of result.results.slice(0, 3)) {
|
||||||
|
contextualThinkingTool.addThinkingStep(thinkingId, {
|
||||||
|
type: 'evidence',
|
||||||
|
content: `Note "${note.title}" (similarity: ${Math.round(note.similarity * 100)}%) contains relevant information`,
|
||||||
|
metadata: {
|
||||||
|
noteId: note.noteId,
|
||||||
|
similarity: note.similarity
|
||||||
|
}
|
||||||
|
}, stepId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
contextualThinkingTool.addThinkingStep(thinkingId, {
|
||||||
|
type: 'observation',
|
||||||
|
content: `No notes found for sub-query: "${result.query}"`,
|
||||||
|
metadata: {
|
||||||
|
searchQuery: result.query
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Get note structure information
|
||||||
|
try {
|
||||||
|
const noteStructure = await noteNavigatorTool.getNoteStructure(noteId);
|
||||||
|
|
||||||
|
contextualThinkingTool.addThinkingStep(thinkingId, {
|
||||||
|
type: 'observation',
|
||||||
|
content: `Note structure: ${noteStructure.childCount} child notes, ${noteStructure.attributes.length} attributes, ${noteStructure.parentPath.length} levels in hierarchy`,
|
||||||
|
metadata: {
|
||||||
|
structure: noteStructure
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add information about parent path
|
||||||
|
if (noteStructure.parentPath.length > 0) {
|
||||||
|
const parentPathStr = noteStructure.parentPath.map((p: {title: string, noteId: string}) => p.title).join(' > ');
|
||||||
|
contextualThinkingTool.addThinkingStep(thinkingId, {
|
||||||
|
type: 'observation',
|
||||||
|
content: `Note hierarchy: ${parentPathStr}`,
|
||||||
|
metadata: {
|
||||||
|
parentPath: noteStructure.parentPath
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Error getting note structure: ${error}`);
|
||||||
|
contextualThinkingTool.addThinkingStep(thinkingId, {
|
||||||
|
type: 'observation',
|
||||||
|
content: `Unable to retrieve note structure information: ${error}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: Conclude thinking process
|
||||||
|
contextualThinkingTool.addThinkingStep(thinkingId, {
|
||||||
|
type: 'conclusion',
|
||||||
|
content: `Analysis complete. Found ${totalResults} relevant notes across ${searchResults.length} search queries.`,
|
||||||
|
metadata: {
|
||||||
|
totalResults,
|
||||||
|
queryCount: searchResults.length
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Complete the thinking process
|
||||||
|
contextualThinkingTool.completeThinking(thinkingId);
|
||||||
|
|
||||||
|
// Step 6: Build the context string combining all the information
|
||||||
|
let agentContext = '';
|
||||||
|
|
||||||
|
// Add note structure information
|
||||||
|
try {
|
||||||
|
const noteStructure = await noteNavigatorTool.getNoteStructure(noteId);
|
||||||
|
agentContext += `## Current Note Context\n`;
|
||||||
|
agentContext += `- Note Title: ${noteStructure.title}\n`;
|
||||||
|
|
||||||
|
if (noteStructure.parentPath.length > 0) {
|
||||||
|
const parentPathStr = noteStructure.parentPath.map((p: {title: string, noteId: string}) => p.title).join(' > ');
|
||||||
|
agentContext += `- Location: ${parentPathStr}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noteStructure.attributes.length > 0) {
|
||||||
|
agentContext += `- Attributes: ${noteStructure.attributes.map((a: {name: string, value: string}) => `${a.name}=${a.value}`).join(', ')}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noteStructure.childCount > 0) {
|
||||||
|
agentContext += `- Contains ${noteStructure.childCount} child notes\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
agentContext += `\n`;
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Error adding note structure to context: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add most relevant notes from search results
|
||||||
|
const allSearchResults = searchResults.flatMap(r => r.results);
|
||||||
|
|
||||||
|
// Deduplicate results by noteId
|
||||||
|
const uniqueResults = new Map();
|
||||||
|
for (const result of allSearchResults) {
|
||||||
|
if (!uniqueResults.has(result.noteId) || uniqueResults.get(result.noteId).similarity < result.similarity) {
|
||||||
|
uniqueResults.set(result.noteId, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by similarity
|
||||||
|
const sortedResults = Array.from(uniqueResults.values())
|
||||||
|
.sort((a, b) => b.similarity - a.similarity)
|
||||||
|
.slice(0, 10); // Get top 10 unique results
|
||||||
|
|
||||||
|
if (sortedResults.length > 0) {
|
||||||
|
agentContext += `## Relevant Information\n`;
|
||||||
|
|
||||||
|
for (const result of sortedResults) {
|
||||||
|
agentContext += `### ${result.title}\n`;
|
||||||
|
|
||||||
|
if (result.content) {
|
||||||
|
// Limit content to 500 chars per note to avoid token explosion
|
||||||
|
agentContext += `${result.content.substring(0, 500)}${result.content.length > 500 ? '...' : ''}\n\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add thinking process if requested
|
||||||
|
if (showThinking) {
|
||||||
|
agentContext += `\n## Reasoning Process\n`;
|
||||||
|
agentContext += contextualThinkingTool.getThinkingSummary(thinkingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log stats about the context
|
||||||
|
log.info(`Agent tools context built: ${agentContext.length} chars, ${agentContext.split('\n').length} lines`);
|
||||||
|
|
||||||
|
return agentContext;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(`Error getting agent tools context: ${error}`);
|
log.error(`Error getting agent tools context: ${error}`);
|
||||||
return '';
|
return `Error generating enhanced context: ${error}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,8 +478,19 @@ export class ContextService {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert parent notes from {id, title} to {noteId, title} for consistency
|
||||||
|
const normalizedRelatedNotes = allRelatedNotes.map(note => {
|
||||||
|
return {
|
||||||
|
noteId: 'id' in note ? note.id : note.noteId,
|
||||||
|
title: note.title
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Rank notes by relevance to query
|
// Rank notes by relevance to query
|
||||||
const rankedNotes = await semanticSearch.rankNotesByRelevance(allRelatedNotes, userQuery);
|
const rankedNotes = await semanticSearch.rankNotesByRelevance(
|
||||||
|
normalizedRelatedNotes as Array<{noteId: string, title: string}>,
|
||||||
|
userQuery
|
||||||
|
);
|
||||||
|
|
||||||
// Get content for the top N most relevant notes
|
// Get content for the top N most relevant notes
|
||||||
const mostRelevantNotes = rankedNotes.slice(0, maxResults);
|
const mostRelevantNotes = rankedNotes.slice(0, maxResults);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user