update agent tools

This commit is contained in:
perf3ct 2025-03-19 20:09:18 +00:00
parent 5b81252959
commit 0d4b6a71fc
No known key found for this signature in database
GPG Key ID: 569C4EEC436F5232
7 changed files with 777 additions and 125 deletions

View File

@ -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",

View File

@ -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;
} }
/** /**

View File

@ -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;

View File

@ -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
}); });

View File

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

View File

@ -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}`;
}
} }
} }

View File

@ -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);