mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-09-26 15:01:32 +08:00
update agent tools
This commit is contained in:
parent
5b81252959
commit
0d4b6a71fc
@ -1836,7 +1836,6 @@
|
||||
"note_chat": "Note Chat",
|
||||
"notes_indexed": "{{ count }} note indexed",
|
||||
"notes_indexed_plural": "{{ count }} notes indexed",
|
||||
"processing": "Processing",
|
||||
"reset_embeddings": "Reset Embeddings",
|
||||
"show_thinking": "Show Thinking",
|
||||
"show_thinking_description": "Reveals the reasoning process used to generate responses",
|
||||
|
@ -49,75 +49,89 @@ export class ContextualThinkingTool {
|
||||
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
|
||||
* @returns The ID of the new thinking process
|
||||
* @param query The user's query
|
||||
* @returns The created thinking process ID
|
||||
*/
|
||||
startThinking(query: string): string {
|
||||
const id = this.generateProcessId();
|
||||
const thinkingId = `thinking_${Date.now()}_${ContextualThinkingTool.thinkingCounter++}`;
|
||||
|
||||
this.processes[id] = {
|
||||
id,
|
||||
log.info(`Starting thinking process: ${thinkingId} for query "${query.substring(0, 50)}..."`);
|
||||
|
||||
this.processes[thinkingId] = {
|
||||
id: thinkingId,
|
||||
query,
|
||||
steps: [],
|
||||
status: 'in_progress',
|
||||
startTime: Date.now()
|
||||
};
|
||||
|
||||
this.activeProcId = id;
|
||||
return id;
|
||||
// Set as active process
|
||||
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 type The type of thinking step
|
||||
* @param options Additional options for the step
|
||||
* @returns The ID of the new step
|
||||
* @param processId The ID of the process to add to
|
||||
* @param step The thinking step to add
|
||||
* @returns The ID of the added step
|
||||
*/
|
||||
addThinkingStep(
|
||||
content: string,
|
||||
type: ThinkingStep['type'],
|
||||
options: {
|
||||
confidence?: number;
|
||||
sources?: string[];
|
||||
parentId?: string;
|
||||
metadata?: Record<string, any>;
|
||||
} = {}
|
||||
): string | null {
|
||||
if (!this.activeProcId || !this.processes[this.activeProcId]) {
|
||||
log.error("No active thinking process to add step to");
|
||||
return null;
|
||||
processId: string,
|
||||
step: Omit<ThinkingStep, 'id'>,
|
||||
parentId?: string
|
||||
): string {
|
||||
const process = this.processes[processId];
|
||||
|
||||
if (!process) {
|
||||
throw new Error(`Thinking process ${processId} not found`);
|
||||
}
|
||||
|
||||
const stepId = this.generateStepId();
|
||||
const step: ThinkingStep = {
|
||||
id: stepId,
|
||||
content,
|
||||
type,
|
||||
...options
|
||||
// Create full step with ID
|
||||
const fullStep: ThinkingStep = {
|
||||
id: `step_${Date.now()}_${Math.floor(Math.random() * 10000)}`,
|
||||
...step,
|
||||
parentId
|
||||
};
|
||||
|
||||
// Add to parent's children if a parent is specified
|
||||
if (options.parentId) {
|
||||
const parentIdx = this.processes[this.activeProcId].steps.findIndex(
|
||||
s => s.id === options.parentId
|
||||
);
|
||||
// Add to process steps
|
||||
process.steps.push(fullStep);
|
||||
|
||||
if (parentIdx >= 0) {
|
||||
const parent = this.processes[this.activeProcId].steps[parentIdx];
|
||||
if (!parent.children) {
|
||||
parent.children = [];
|
||||
// If this step has a parent, update the parent's children list
|
||||
if (parentId) {
|
||||
const parentStep = process.steps.find(s => s.id === parentId);
|
||||
if (parentStep) {
|
||||
if (!parentStep.children) {
|
||||
parentStep.children = [];
|
||||
}
|
||||
parent.children.push(stepId);
|
||||
this.processes[this.activeProcId].steps[parentIdx] = parent;
|
||||
parentStep.children.push(fullStep.id);
|
||||
}
|
||||
}
|
||||
|
||||
this.processes[this.activeProcId].steps.push(step);
|
||||
return stepId;
|
||||
// Log the step addition with more detail
|
||||
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)}..."`);
|
||||
|
||||
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) {
|
||||
html += `<div class='thinking-step ${step.type || ""}'>`;
|
||||
// Show overall time taken for the thinking process
|
||||
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
|
||||
const icon = this.getStepIcon(step.type);
|
||||
html += `<span class='bx ${icon}'></span> `;
|
||||
stepHtml += `<span class='bx ${icon}'></span> `;
|
||||
|
||||
// Add the step content
|
||||
html += step.content;
|
||||
stepHtml += step.content;
|
||||
|
||||
// Show confidence if available
|
||||
if (step.metadata?.confidence) {
|
||||
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>";
|
||||
@ -232,7 +292,61 @@ export class ContextualThinkingTool {
|
||||
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[];
|
||||
}
|
||||
|
||||
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 {
|
||||
private maxPathLength: number = 20;
|
||||
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)
|
||||
* 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)
|
||||
*/
|
||||
getNoteClones(noteId: string): NoteInfo[] {
|
||||
async getNoteClones(noteId: string): Promise<NoteInfo[]> {
|
||||
try {
|
||||
const note = becca.notes[noteId];
|
||||
if (!note) {
|
||||
@ -362,7 +332,10 @@ export class NoteNavigatorTool {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
log.error(`Error getting note clones: ${error.message}`);
|
||||
return [];
|
||||
@ -373,7 +346,7 @@ export class NoteNavigatorTool {
|
||||
* 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
|
||||
*/
|
||||
getNoteContextDescription(noteId: string): string {
|
||||
async getNoteContextDescription(noteId: string): Promise<string> {
|
||||
try {
|
||||
const note = becca.notes[noteId];
|
||||
if (!note) {
|
||||
@ -409,8 +382,9 @@ export class NoteNavigatorTool {
|
||||
result += `Path: ${path.notePathTitles.join(' > ')}\n`;
|
||||
}
|
||||
|
||||
// Children info
|
||||
const children = this.getChildNotes(noteId, 5);
|
||||
// Children info using the async function
|
||||
const children = await this.getChildNotes(noteId, 5);
|
||||
|
||||
if (children.length > 0) {
|
||||
result += `\nContains ${note.children.length} child notes`;
|
||||
if (children.length < note.children.length) {
|
||||
@ -419,7 +393,7 @@ export class NoteNavigatorTool {
|
||||
result += `:\n`;
|
||||
|
||||
for (const child of children) {
|
||||
result += `- ${child.title} (${child.type})\n`;
|
||||
result += `- ${child.title}\n`;
|
||||
}
|
||||
|
||||
if (children.length < note.children.length) {
|
||||
@ -458,6 +432,185 @@ export class NoteNavigatorTool {
|
||||
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;
|
||||
|
@ -41,11 +41,17 @@ export class QueryDecompositionTool {
|
||||
*/
|
||||
decomposeQuery(query: string, context?: string): DecomposedQuery {
|
||||
try {
|
||||
// Log the decomposition attempt for tracking
|
||||
log.info(`Decomposing query: "${query.substring(0, 100)}..."`);
|
||||
|
||||
// Assess query complexity to determine if decomposition is needed
|
||||
const complexity = this.assessQueryComplexity(query);
|
||||
log.info(`Query complexity assessment: ${complexity}/10`);
|
||||
|
||||
// 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 {
|
||||
originalQuery: query,
|
||||
subQueries: [{
|
||||
@ -61,6 +67,12 @@ export class QueryDecompositionTool {
|
||||
|
||||
// For complex queries, perform decomposition
|
||||
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 {
|
||||
originalQuery: query,
|
||||
@ -248,9 +260,18 @@ export class QueryDecompositionTool {
|
||||
private createSubQueries(query: string, context?: string): SubQuery[] {
|
||||
const subQueries: SubQuery[] = [];
|
||||
|
||||
// Simple heuristics for breaking down the query
|
||||
// In a real implementation, this would be much more sophisticated,
|
||||
// using natural language understanding to identify different intents
|
||||
// Use context to enhance sub-query generation if available
|
||||
if (context) {
|
||||
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
|
||||
const questionSplit = query.split(/\?/).filter(q => q.trim().length > 0);
|
||||
@ -266,6 +287,15 @@ export class QueryDecompositionTool {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -305,8 +335,22 @@ export class QueryDecompositionTool {
|
||||
|
||||
subQueries.push({
|
||||
id: this.generateSubQueryId(),
|
||||
text: `What are the main differences and similarities between ${item1} and ${item2}?`,
|
||||
reason: 'Direct comparison after understanding each item',
|
||||
text: `What are the main differences between ${item1} and ${item2}?`,
|
||||
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
|
||||
});
|
||||
|
||||
@ -317,7 +361,8 @@ export class QueryDecompositionTool {
|
||||
}
|
||||
|
||||
// 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
|
||||
const words = query.toLowerCase().split(/\W+/).filter(w =>
|
||||
w.length > 3 &&
|
||||
@ -333,7 +378,7 @@ export class QueryDecompositionTool {
|
||||
// Get top frequent words
|
||||
const topWords = Object.entries(wordFrequency)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 3)
|
||||
.slice(0, 4) // Increased from 3 to 4
|
||||
.map(entry => entry[0]);
|
||||
|
||||
if (topWords.length > 0) {
|
||||
@ -345,16 +390,38 @@ export class QueryDecompositionTool {
|
||||
isAnswered: false
|
||||
});
|
||||
|
||||
// Create relationship sub-query if multiple top words
|
||||
if (topWords.length > 1) {
|
||||
// Add individual queries for each key topic
|
||||
topWords.forEach(word => {
|
||||
subQueries.push({
|
||||
id: this.generateSubQueryId(),
|
||||
text: `How do ${topWords.join(' and ')} relate to each other?`,
|
||||
reason: 'Understanding relationships between key topics',
|
||||
text: `What specific details about "${word}" are most relevant to the query?`,
|
||||
reason: `Detailed exploration of the "${word}" concept`,
|
||||
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
|
||||
subQueries.push({
|
||||
id: this.generateSubQueryId(),
|
||||
@ -368,10 +435,26 @@ export class QueryDecompositionTool {
|
||||
}
|
||||
|
||||
// Fallback: If we can't meaningfully decompose, just use the original query
|
||||
// But also add some generic exploration questions
|
||||
subQueries.push({
|
||||
id: this.generateSubQueryId(),
|
||||
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
|
||||
});
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
*/
|
||||
|
||||
import log from '../../log.js';
|
||||
import type { ContextService } from '../context/modules/context_service.js';
|
||||
|
||||
// Define interface for context service to avoid circular imports
|
||||
interface IContextService {
|
||||
@ -48,6 +49,12 @@ export interface ChunkSearchResultItem {
|
||||
parentId?: string;
|
||||
}
|
||||
|
||||
export interface VectorSearchOptions {
|
||||
limit?: number;
|
||||
threshold?: number;
|
||||
includeContent?: boolean;
|
||||
}
|
||||
|
||||
export class VectorSearchTool {
|
||||
private contextService: IContextService | null = null;
|
||||
private maxResults: number = 5;
|
||||
@ -64,6 +71,77 @@ export class 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
|
||||
*/
|
||||
|
@ -414,12 +414,19 @@ export class AIServiceManager {
|
||||
showThinking: boolean = false,
|
||||
relevantNotes: Array<any> = []
|
||||
): Promise<string> {
|
||||
return contextService.getAgentToolsContext(
|
||||
noteId,
|
||||
query,
|
||||
showThinking,
|
||||
relevantNotes
|
||||
);
|
||||
// Just use the context service directly
|
||||
try {
|
||||
const cs = (await import('./context/modules/context_service.js')).default;
|
||||
return cs.getAgentToolsContext(
|
||||
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> = []
|
||||
): Promise<string> {
|
||||
try {
|
||||
return await aiServiceManager.getInstance().getAgentToolsContext(
|
||||
noteId,
|
||||
query,
|
||||
showThinking,
|
||||
relevantNotes
|
||||
);
|
||||
log.info(`Building enhanced agent tools context for query: "${query.substring(0, 50)}...", noteId=${noteId}, showThinking=${showThinking}`);
|
||||
|
||||
// Make sure agent tools are initialized
|
||||
const agentManager = aiServiceManager.getInstance();
|
||||
|
||||
// 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) {
|
||||
log.error(`Error getting agent tools context: ${error}`);
|
||||
return '';
|
||||
return `Error generating enhanced context: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -271,8 +478,19 @@ export class ContextService {
|
||||
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
|
||||
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
|
||||
const mostRelevantNotes = rankedNotes.slice(0, maxResults);
|
||||
|
Loading…
x
Reference in New Issue
Block a user