agent tools do something now

This commit is contained in:
perf3ct 2025-03-19 20:17:52 +00:00
parent 0d4b6a71fc
commit 90db570e30
No known key found for this signature in database
GPG Key ID: 569C4EEC436F5232
4 changed files with 820 additions and 767 deletions

View File

@ -20,388 +20,392 @@ import aiServiceManager from "../ai_service_manager.js";
* Represents a single reasoning step taken by the agent * Represents a single reasoning step taken by the agent
*/ */
export interface ThinkingStep { export interface ThinkingStep {
id: string; id: string;
content: string; content: string;
type: 'observation' | 'hypothesis' | 'question' | 'evidence' | 'conclusion'; type: 'observation' | 'hypothesis' | 'question' | 'evidence' | 'conclusion';
confidence?: number; confidence?: number;
sources?: string[]; sources?: string[];
parentId?: string; parentId?: string;
children?: string[]; children?: string[];
metadata?: Record<string, any>; metadata?: Record<string, any>;
} }
/** /**
* Contains the full reasoning process * Contains the full reasoning process
*/ */
export interface ThinkingProcess { export interface ThinkingProcess {
id: string; id: string;
query: string; query: string;
steps: ThinkingStep[]; steps: ThinkingStep[];
status: 'in_progress' | 'completed'; status: 'in_progress' | 'completed';
startTime: number; startTime: number;
endTime?: number; endTime?: number;
} }
export class ContextualThinkingTool { export class ContextualThinkingTool {
private static thinkingCounter = 0; private static thinkingCounter = 0;
private static stepCounter = 0; private static stepCounter = 0;
private activeProcId?: string; private activeProcId?: string;
private processes: Record<string, ThinkingProcess> = {}; private processes: Record<string, ThinkingProcess> = {};
/** /**
* Start a new thinking process for a query * Start a new thinking process for a query
* *
* @param query The user's query * @param query The user's query
* @returns The created thinking process ID * @returns The created thinking process ID
*/ */
startThinking(query: string): string { startThinking(query: string): string {
const thinkingId = `thinking_${Date.now()}_${ContextualThinkingTool.thinkingCounter++}`; const thinkingId = `thinking_${Date.now()}_${ContextualThinkingTool.thinkingCounter++}`;
log.info(`Starting thinking process: ${thinkingId} for query "${query.substring(0, 50)}..."`); log.info(`Starting thinking process: ${thinkingId} for query "${query.substring(0, 50)}..."`);
this.processes[thinkingId] = { this.processes[thinkingId] = {
id: thinkingId, id: thinkingId,
query, query,
steps: [], steps: [],
status: 'in_progress', status: 'in_progress',
startTime: Date.now() startTime: Date.now()
}; };
// Set as active process // Set as active process
this.activeProcId = thinkingId; this.activeProcId = thinkingId;
// Initialize with some starter thinking steps // Initialize with some starter thinking steps
this.addThinkingStep(thinkingId, { this.addThinkingStep(thinkingId, {
type: 'observation', type: 'observation',
content: `Starting analysis of the query: "${query}"` content: `Starting analysis of the query: "${query}"`
}); });
this.addThinkingStep(thinkingId, { this.addThinkingStep(thinkingId, {
type: 'question', type: 'question',
content: `What are the key components of this query that need to be addressed?` content: `What are the key components of this query that need to be addressed?`
}); });
this.addThinkingStep(thinkingId, { this.addThinkingStep(thinkingId, {
type: 'observation', type: 'observation',
content: `Breaking down the query to understand its requirements and context.` content: `Breaking down the query to understand its requirements and context.`
}); });
return thinkingId; return thinkingId;
}
/**
* Add a thinking step to a process
*
* @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(
processId: string,
step: Omit<ThinkingStep, 'id'>,
parentId?: string
): string {
const process = this.processes[processId];
if (!process) {
throw new Error(`Thinking process ${processId} not found`);
} }
// Create full step with ID /**
const fullStep: ThinkingStep = { * Add a thinking step to a process
id: `step_${Date.now()}_${Math.floor(Math.random() * 10000)}`, *
...step, * @param processId The ID of the process to add to
parentId * @param step The thinking step to add
}; * @returns The ID of the added step
*/
addThinkingStep(
processId: string,
step: Omit<ThinkingStep, 'id'>,
parentId?: string
): string {
const process = this.processes[processId];
// Add to process steps if (!process) {
process.steps.push(fullStep); throw new Error(`Thinking process ${processId} not found`);
// 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 = [];
} }
parentStep.children.push(fullStep.id);
}
}
// Log the step addition with more detail // Create full step with ID
log.info(`Added thinking step to process ${processId}: [${step.type}] ${step.content.substring(0, 100)}...`); const fullStep: ThinkingStep = {
id: `step_${Date.now()}_${Math.floor(Math.random() * 10000)}`,
...step,
parentId
};
return fullStep.id; // Add to process steps
} process.steps.push(fullStep);
/** // If this step has a parent, update the parent's children list
* Complete the current thinking process if (parentId) {
* const parentStep = process.steps.find(s => s.id === parentId);
* @param processId The ID of the process to complete (defaults to active process) if (parentStep) {
* @returns The completed thinking process if (!parentStep.children) {
*/ parentStep.children = [];
completeThinking(processId?: string): ThinkingProcess | null { }
const id = processId || this.activeProcId; parentStep.children.push(fullStep.id);
}
if (!id || !this.processes[id]) {
log.error(`Thinking process ${id} not found`);
return null;
}
this.processes[id].status = 'completed';
this.processes[id].endTime = Date.now();
if (id === this.activeProcId) {
this.activeProcId = undefined;
}
return this.processes[id];
}
/**
* Get a thinking process by ID
*/
getThinkingProcess(processId: string): ThinkingProcess | null {
return this.processes[processId] || null;
}
/**
* Get the active thinking process
*/
getActiveThinkingProcess(): ThinkingProcess | null {
if (!this.activeProcId) return null;
return this.processes[this.activeProcId] || null;
}
/**
* Visualize the thinking process as HTML for display in the UI
*
* @param thinkingId The ID of the thinking process to visualize
* @returns HTML representation of the thinking process
*/
visualizeThinking(thinkingId: string): string {
log.info(`Visualizing thinking process: thinkingId=${thinkingId}`);
const process = this.getThinkingProcess(thinkingId);
if (!process) {
log.info(`No thinking process found for id: ${thinkingId}`);
return "<div class='thinking-process'>No thinking process found</div>";
}
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>Reasoning Process</h4>`;
html += `<div class='thinking-query'>${process.query}</div>`;
// 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);
stepHtml += `<span class='bx ${icon}'></span> `;
// Add the step content
stepHtml += step.content;
// Show confidence if available
if (step.metadata?.confidence) {
const confidence = Math.round((step.metadata.confidence as number) * 100);
stepHtml += ` <span class='thinking-confidence'>(Confidence: ${confidence}%)</span>`;
}
// 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; // Log the step addition with more detail
}; log.info(`Added thinking step to process ${processId}: [${step.type}] ${step.content.substring(0, 100)}...`);
// Render top-level steps and their children return fullStep.id;
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) { * Complete the current thinking process
html += renderStepWithChildren(childId, 1); *
* @param processId The ID of the process to complete (defaults to active process)
* @returns The completed thinking process
*/
completeThinking(processId?: string): ThinkingProcess | null {
const id = processId || this.activeProcId;
if (!id || !this.processes[id]) {
log.error(`Thinking process ${id} not found`);
return null;
} }
}
this.processes[id].status = 'completed';
this.processes[id].endTime = Date.now();
if (id === this.activeProcId) {
this.activeProcId = undefined;
}
return this.processes[id];
} }
html += "</div>"; /**
return html; * Get a thinking process by ID
} */
getThinkingProcess(processId: string): ThinkingProcess | null {
/** return this.processes[processId] || null;
* Get an appropriate icon for a thinking step type
*/
private getStepIcon(type: string): string {
switch (type) {
case 'observation':
return 'bx-search';
case 'hypothesis':
return 'bx-bulb';
case 'evidence':
return 'bx-list-check';
case 'conclusion':
return 'bx-check-circle';
default:
return 'bx-message-square-dots';
}
}
/**
* Get a plain text summary of the thinking process
*
* @param thinkingId The ID of the thinking process to summarize
* @returns Text summary of the thinking process
*/
getThinkingSummary(thinkingId: string): string {
const process = this.getThinkingProcess(thinkingId);
if (!process) {
return "No thinking process available.";
} }
let summary = `## Reasoning Process for Query: "${process.query}"\n\n`; /**
* Get the active thinking process
// Group steps by type for better organization */
const observations = process.steps.filter(s => s.type === 'observation'); getActiveThinkingProcess(): ThinkingProcess | null {
const questions = process.steps.filter(s => s.type === 'question'); if (!this.activeProcId) return null;
const hypotheses = process.steps.filter(s => s.type === 'hypothesis'); return this.processes[this.activeProcId] || null;
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) { * Visualize the thinking process as HTML for display in the UI
summary += "### Questions Considered:\n"; *
questions.forEach(step => { * @param thinkingId The ID of the thinking process to visualize
summary += `- ${step.content}\n`; * @returns HTML representation of the thinking process
}); */
summary += "\n"; visualizeThinking(thinkingId: string): string {
log.info(`Visualizing thinking process: thinkingId=${thinkingId}`);
const process = this.getThinkingProcess(thinkingId);
if (!process) {
log.info(`No thinking process found for id: ${thinkingId}`);
return "<div class='thinking-process'>No thinking process found</div>";
}
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>Reasoning Process</h4>`;
html += `<div class='thinking-query'>${process.query}</div>`;
// 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);
stepHtml += `<span class='bx ${icon}'></span> `;
// Add the step content
stepHtml += step.content;
// Show confidence if available
if (step.metadata?.confidence) {
const confidence = Math.round((step.metadata.confidence as number) * 100);
stepHtml += ` <span class='thinking-confidence'>(Confidence: ${confidence}%)</span>`;
}
// 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>";
return html;
} }
// Add hypotheses /**
if (hypotheses.length > 0) { * Get an appropriate icon for a thinking step type
summary += "### Hypotheses:\n"; */
hypotheses.forEach(step => { private getStepIcon(type: string): string {
summary += `- ${step.content}\n`; switch (type) {
}); case 'observation':
summary += "\n"; return 'bx-search';
case 'hypothesis':
return 'bx-bulb';
case 'evidence':
return 'bx-list-check';
case 'conclusion':
return 'bx-check-circle';
default:
return 'bx-message-square-dots';
}
} }
// Add evidence /**
if (evidence.length > 0) { * Get a plain text summary of the thinking process
summary += "### Evidence Gathered:\n"; *
evidence.forEach(step => { * @param thinkingId The ID of the thinking process to summarize
summary += `- ${step.content}\n`; * @returns Text summary of the thinking process
}); */
summary += "\n"; getThinkingSummary(thinkingId: string): string {
const process = this.getThinkingProcess(thinkingId);
if (!process) {
log.error(`No thinking process found for id: ${thinkingId}`);
return "No thinking process available.";
}
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');
log.info(`Generating thinking summary with: ${observations.length} observations, ${questions.length} questions, ${hypotheses.length} hypotheses, ${evidence.length} evidence, ${conclusions.length} conclusions`);
// 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";
}
log.info(`Generated thinking summary with ${summary.length} characters`);
return summary;
} }
// Add conclusions /**
if (conclusions.length > 0) { * Reset the active thinking process
summary += "### Conclusions:\n"; */
conclusions.forEach(step => { resetActiveThinking(): void {
summary += `- ${step.content}\n`; this.activeProcId = undefined;
});
summary += "\n";
} }
return summary; /**
} * Generate a unique ID for a thinking process
*/
/** private generateProcessId(): string {
* Reset the active thinking process return `thinking_${Date.now()}_${ContextualThinkingTool.thinkingCounter++}`;
*/
resetActiveThinking(): void {
this.activeProcId = undefined;
}
/**
* Generate a unique ID for a thinking process
*/
private generateProcessId(): string {
return `thinking_${Date.now()}_${ContextualThinkingTool.thinkingCounter++}`;
}
/**
* Generate a unique ID for a thinking step
*/
private generateStepId(): string {
return `step_${Date.now()}_${ContextualThinkingTool.stepCounter++}`;
}
/**
* Format duration between two timestamps
*/
private formatDuration(start: number, end: number): string {
const durationMs = end - start;
if (durationMs < 1000) {
return `${durationMs}ms`;
} else if (durationMs < 60000) {
return `${Math.round(durationMs / 1000)}s`;
} else {
return `${Math.round(durationMs / 60000)}m ${Math.round((durationMs % 60000) / 1000)}s`;
} }
}
/** /**
* Recursively render a step and its children * Generate a unique ID for a thinking step
*/ */
private renderStepTree(step: ThinkingStep, allSteps: ThinkingStep[]): string { private generateStepId(): string {
const typeIcons: Record<string, string> = { return `step_${Date.now()}_${ContextualThinkingTool.stepCounter++}`;
'observation': '🔍', }
'hypothesis': '🤔',
'question': '❓',
'evidence': '📋',
'conclusion': '✅'
};
const icon = typeIcons[step.type] || '•'; /**
const confidenceDisplay = step.confidence !== undefined * Format duration between two timestamps
? `<span class="confidence">${Math.round(step.confidence * 100)}%</span>` */
: ''; private formatDuration(start: number, end: number): string {
const durationMs = end - start;
if (durationMs < 1000) {
return `${durationMs}ms`;
} else if (durationMs < 60000) {
return `${Math.round(durationMs / 1000)}s`;
} else {
return `${Math.round(durationMs / 60000)}m ${Math.round((durationMs % 60000) / 1000)}s`;
}
}
let html = ` /**
* Recursively render a step and its children
*/
private renderStepTree(step: ThinkingStep, allSteps: ThinkingStep[]): string {
const typeIcons: Record<string, string> = {
'observation': '🔍',
'hypothesis': '🤔',
'question': '❓',
'evidence': '📋',
'conclusion': '✅'
};
const icon = typeIcons[step.type] || '•';
const confidenceDisplay = step.confidence !== undefined
? `<span class="confidence">${Math.round(step.confidence * 100)}%</span>`
: '';
let html = `
<div class="thinking-step thinking-${step.type}"> <div class="thinking-step thinking-${step.type}">
<div class="step-header"> <div class="step-header">
<span class="step-icon">${icon}</span> <span class="step-icon">${icon}</span>
@ -411,28 +415,28 @@ export class ContextualThinkingTool {
<div class="step-content">${step.content}</div> <div class="step-content">${step.content}</div>
`; `;
// Add sources if available // Add sources if available
if (step.sources && step.sources.length > 0) { if (step.sources && step.sources.length > 0) {
html += `<div class="step-sources">Sources: ${step.sources.join(', ')}</div>`; html += `<div class="step-sources">Sources: ${step.sources.join(', ')}</div>`;
}
// Recursively render children
if (step.children && step.children.length > 0) {
html += `<div class="step-children">`;
for (const childId of step.children) {
const childStep = allSteps.find(s => s.id === childId);
if (childStep) {
html += this.renderStepTree(childStep, allSteps);
} }
}
html += `</div>`; // Recursively render children
if (step.children && step.children.length > 0) {
html += `<div class="step-children">`;
for (const childId of step.children) {
const childStep = allSteps.find(s => s.id === childId);
if (childStep) {
html += this.renderStepTree(childStep, allSteps);
}
}
html += `</div>`;
}
html += `</div>`;
return html;
} }
html += `</div>`;
return html;
}
} }
export default ContextualThinkingTool; export default ContextualThinkingTool;

View File

@ -443,11 +443,32 @@ export class NoteNavigatorTool {
try { try {
log.info(`Getting note structure for note ${noteId}`); log.info(`Getting note structure for note ${noteId}`);
// Special handling for 'root' or other special notes
if (noteId === 'root' || !noteId) {
log.info('Using root as the special note for structure');
return {
noteId: 'root',
title: 'Root',
type: 'root',
childCount: 0, // We don't know how many direct children root has
attributes: [],
parentPath: []
};
}
// Get the note from becca // Get the note from becca
const note = becca.notes[noteId]; const note = becca.notes[noteId];
if (!note) { if (!note) {
throw new Error(`Note ${noteId} not found`); log.error(`Note ${noteId} not found in becca.notes`);
return {
noteId,
title: 'Unknown',
type: 'unknown',
childCount: 0,
attributes: [],
parentPath: []
};
} }
// Get child notes count // Get child notes count

View File

@ -15,459 +15,480 @@
import log from '../../log.js'; import log from '../../log.js';
export interface SubQuery { export interface SubQuery {
id: string; id: string;
text: string; text: string;
reason: string; reason: string;
isAnswered: boolean; isAnswered: boolean;
answer?: string; answer?: string;
} }
export interface DecomposedQuery { export interface DecomposedQuery {
originalQuery: string; originalQuery: string;
subQueries: SubQuery[]; subQueries: SubQuery[];
status: 'pending' | 'in_progress' | 'completed'; status: 'pending' | 'in_progress' | 'completed';
complexity: number; complexity: number;
} }
export class QueryDecompositionTool { export class QueryDecompositionTool {
private static queryCounter: number = 0; private static queryCounter: number = 0;
/** /**
* Break down a complex query into smaller, more manageable sub-queries * Break down a complex query into smaller, more manageable sub-queries
* *
* @param query The original user query * @param query The original user query
* @param context Optional context about the current note being viewed * @param context Optional context about the current note being viewed
* @returns A decomposed query object with sub-queries * @returns A decomposed query object with sub-queries
*/ */
decomposeQuery(query: string, context?: string): DecomposedQuery { decomposeQuery(query: string, context?: string): DecomposedQuery {
try { try {
// Log the decomposition attempt for tracking // Log the decomposition attempt for tracking
log.info(`Decomposing query: "${query.substring(0, 100)}..."`); log.info(`Decomposing query: "${query.substring(0, 100)}..."`);
// Assess query complexity to determine if decomposition is needed if (!query || query.trim().length === 0) {
const complexity = this.assessQueryComplexity(query); log.info("Query decomposition called with empty query");
log.info(`Query complexity assessment: ${complexity}/10`); return {
originalQuery: query,
subQueries: [],
status: 'pending',
complexity: 0
};
}
// For simple queries, just return the original as a single sub-query // Assess query complexity to determine if decomposition is needed
// Use a lower threshold (2 instead of 3) to decompose more queries const complexity = this.assessQueryComplexity(query);
if (complexity < 2) { log.info(`Query complexity assessment: ${complexity}/10`);
log.info(`Query is simple (complexity ${complexity}), returning as single sub-query`);
return {
originalQuery: query,
subQueries: [{
id: this.generateSubQueryId(),
text: query,
reason: 'Direct question that can be answered without decomposition',
isAnswered: false
}],
status: 'pending',
complexity
};
}
// For complex queries, perform decomposition // For simple queries, just return the original as a single sub-query
const subQueries = this.createSubQueries(query, context); // Use a lower threshold (2 instead of 3) to decompose more queries
log.info(`Decomposed query into ${subQueries.length} sub-queries`); if (complexity < 2) {
log.info(`Query is simple (complexity ${complexity}), returning as single sub-query`);
// Log the sub-queries for better visibility const mainSubQuery = {
subQueries.forEach((sq, index) => { id: this.generateSubQueryId(),
log.info(`Sub-query ${index + 1}: "${sq.text}" - Reason: ${sq.reason}`); text: query,
}); reason: 'Direct question that can be answered without decomposition',
isAnswered: false
};
return { // Still add a generic exploration query to get some related content
originalQuery: query, const genericQuery = {
subQueries, id: this.generateSubQueryId(),
status: 'pending', text: `Information related to ${query}`,
complexity reason: "Generic exploration to find related content",
}; isAnswered: false
} catch (error: any) { };
log.error(`Error decomposing query: ${error.message}`);
// Fallback to treating it as a simple query return {
return { originalQuery: query,
originalQuery: query, subQueries: [mainSubQuery, genericQuery],
subQueries: [{ status: 'pending',
id: this.generateSubQueryId(), complexity
text: query, };
reason: 'Error in decomposition, treating as simple query', }
isAnswered: false
}],
status: 'pending',
complexity: 1
};
}
}
/** // For complex queries, perform decomposition
* Update a sub-query with its answer const subQueries = this.createSubQueries(query, context);
* log.info(`Decomposed query into ${subQueries.length} sub-queries`);
* @param decomposedQuery The decomposed query object
* @param subQueryId The ID of the sub-query to update
* @param answer The answer to the sub-query
* @returns The updated decomposed query
*/
updateSubQueryAnswer(
decomposedQuery: DecomposedQuery,
subQueryId: string,
answer: string
): DecomposedQuery {
const updatedSubQueries = decomposedQuery.subQueries.map(sq => {
if (sq.id === subQueryId) {
return {
...sq,
answer,
isAnswered: true
};
}
return sq;
});
// Check if all sub-queries are answered // Log the sub-queries for better visibility
const allAnswered = updatedSubQueries.every(sq => sq.isAnswered); subQueries.forEach((sq, index) => {
log.info(`Sub-query ${index + 1}: "${sq.text}" - Reason: ${sq.reason}`);
});
return { return {
...decomposedQuery, originalQuery: query,
subQueries: updatedSubQueries, subQueries,
status: allAnswered ? 'completed' : 'in_progress' status: 'pending',
}; complexity
} };
} catch (error: any) {
log.error(`Error decomposing query: ${error.message}`);
/** // Fallback to treating it as a simple query
* Synthesize all sub-query answers into a comprehensive response return {
* originalQuery: query,
* @param decomposedQuery The decomposed query with all sub-queries answered subQueries: [{
* @returns A synthesized answer to the original query id: this.generateSubQueryId(),
*/ text: query,
synthesizeAnswer(decomposedQuery: DecomposedQuery): string { reason: 'Error in decomposition, treating as simple query',
try { isAnswered: false
// Ensure all sub-queries are answered }],
if (!decomposedQuery.subQueries.every(sq => sq.isAnswered)) { status: 'pending',
return "Cannot synthesize answer - not all sub-queries have been answered."; complexity: 1
} };
// For simple queries with just one sub-query, return the answer directly
if (decomposedQuery.subQueries.length === 1) {
return decomposedQuery.subQueries[0].answer || "";
}
// For complex queries, build a structured response that references each sub-answer
let synthesized = `Answer to: "${decomposedQuery.originalQuery}"\n\n`;
// Group by themes if there are many sub-queries
if (decomposedQuery.subQueries.length > 3) {
// Here we would ideally group related sub-queries, but for now we'll just present them in order
synthesized += "Based on the information gathered:\n\n";
for (const sq of decomposedQuery.subQueries) {
synthesized += `${sq.answer}\n\n`;
} }
} else {
// For fewer sub-queries, present each one with its question
for (const sq of decomposedQuery.subQueries) {
synthesized += `${sq.answer}\n\n`;
}
}
return synthesized.trim();
} catch (error: any) {
log.error(`Error synthesizing answer: ${error.message}`);
return "Error synthesizing the final answer.";
}
}
/**
* Generate a status report on the progress of answering a complex query
*
* @param decomposedQuery The decomposed query
* @returns A status report string
*/
getQueryStatus(decomposedQuery: DecomposedQuery): string {
const answeredCount = decomposedQuery.subQueries.filter(sq => sq.isAnswered).length;
const totalCount = decomposedQuery.subQueries.length;
let status = `Progress: ${answeredCount}/${totalCount} sub-queries answered\n\n`;
for (const sq of decomposedQuery.subQueries) {
status += `${sq.isAnswered ? '✓' : '○'} ${sq.text}\n`;
if (sq.isAnswered) {
status += ` Answer: ${this.truncateText(sq.answer || "", 100)}\n`;
}
} }
return status; /**
} * Update a sub-query with its answer
*
/** * @param decomposedQuery The decomposed query object
* Assess the complexity of a query on a scale of 1-10 * @param subQueryId The ID of the sub-query to update
* This helps determine how many sub-queries are needed * @param answer The answer to the sub-query
* * @returns The updated decomposed query
* @param query The query to assess */
* @returns A complexity score from 1-10 updateSubQueryAnswer(
*/ decomposedQuery: DecomposedQuery,
assessQueryComplexity(query: string): number { subQueryId: string,
// Count the number of question marks as a basic indicator answer: string
const questionMarkCount = (query.match(/\?/g) || []).length; ): DecomposedQuery {
const updatedSubQueries = decomposedQuery.subQueries.map(sq => {
// Count potential sub-questions based on question words if (sq.id === subQueryId) {
const questionWords = ['what', 'how', 'why', 'where', 'when', 'who', 'which']; return {
const questionWordMatches = questionWords.map(word => { ...sq,
const regex = new RegExp(`\\b${word}\\b`, 'gi'); answer,
return (query.match(regex) || []).length; isAnswered: true
}); };
}
const questionWordCount = questionWordMatches.reduce((sum, count) => sum + count, 0); return sq;
// Look for conjunctions which might join multiple questions
const conjunctionCount = (query.match(/\b(and|or|but|as well as)\b/gi) || []).length;
// Look for complex requirements
const comparisonCount = (query.match(/\b(compare|versus|vs|difference|similarities?)\b/gi) || []).length;
const analysisCount = (query.match(/\b(analyze|examine|investigate|explore|explain|discuss)\b/gi) || []).length;
// Calculate base complexity
let complexity = 1;
// Add for multiple questions
complexity += Math.min(2, questionMarkCount);
// Add for question words beyond the first one
complexity += Math.min(2, Math.max(0, questionWordCount - 1));
// Add for conjunctions that might join questions
complexity += Math.min(2, conjunctionCount);
// Add for comparative/analytical requirements
complexity += Math.min(2, comparisonCount + analysisCount);
// Add for overall length/complexity
if (query.length > 100) complexity += 1;
if (query.length > 200) complexity += 1;
// Ensure we stay in the 1-10 range
return Math.max(1, Math.min(10, complexity));
}
/**
* Generate a unique ID for a sub-query
*/
private generateSubQueryId(): string {
return `sq_${Date.now()}_${QueryDecompositionTool.queryCounter++}`;
}
/**
* Create sub-queries based on the original query and optional context
*/
private createSubQueries(query: string, context?: string): SubQuery[] {
const subQueries: SubQuery[] = [];
// 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);
if (questionSplit.length > 1) {
// Multiple distinct questions detected
for (let i = 0; i < questionSplit.length; i++) {
const text = questionSplit[i].trim() + '?';
subQueries.push({
id: this.generateSubQueryId(),
text,
reason: `Separate question ${i+1} detected in the original query`,
isAnswered: false
}); });
}
// Also add a synthesis question // Check if all sub-queries are answered
subQueries.push({ const allAnswered = updatedSubQueries.every(sq => sq.isAnswered);
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 {
...decomposedQuery,
subQueries: updatedSubQueries,
status: allAnswered ? 'completed' : 'in_progress'
};
} }
// 2. Look for "and", "or", etc. connecting potentially separate questions /**
const conjunctions = [ * Synthesize all sub-query answers into a comprehensive response
{ regex: /\b(compare|versus|vs\.?|difference between|similarities between)\b/i, label: 'comparison' }, *
{ regex: /\b(list|enumerate)\b/i, label: 'listing' }, * @param decomposedQuery The decomposed query with all sub-queries answered
{ regex: /\b(analyze|examine|investigate|explore)\b/i, label: 'analysis' }, * @returns A synthesized answer to the original query
{ regex: /\b(explain|why)\b/i, label: 'explanation' }, */
{ regex: /\b(how to|steps to|process of)\b/i, label: 'procedure' } synthesizeAnswer(decomposedQuery: DecomposedQuery): string {
]; try {
// Ensure all sub-queries are answered
if (!decomposedQuery.subQueries.every(sq => sq.isAnswered)) {
return "Cannot synthesize answer - not all sub-queries have been answered.";
}
// Check for comparison queries - these often need multiple sub-queries // For simple queries with just one sub-query, return the answer directly
for (const conj of conjunctions) { if (decomposedQuery.subQueries.length === 1) {
if (conj.regex.test(query)) { return decomposedQuery.subQueries[0].answer || "";
if (conj.label === 'comparison') { }
// For comparisons, we need to research each item, then compare them
const comparisonMatch = query.match(/\b(compare|versus|vs\.?|difference between|similarities between)\s+(.+?)\s+(and|with|to)\s+(.+?)(\?|$)/i);
if (comparisonMatch) { // For complex queries, build a structured response that references each sub-answer
const item1 = comparisonMatch[2].trim(); let synthesized = `Answer to: "${decomposedQuery.originalQuery}"\n\n`;
const item2 = comparisonMatch[4].trim();
// Group by themes if there are many sub-queries
if (decomposedQuery.subQueries.length > 3) {
// Here we would ideally group related sub-queries, but for now we'll just present them in order
synthesized += "Based on the information gathered:\n\n";
for (const sq of decomposedQuery.subQueries) {
synthesized += `${sq.answer}\n\n`;
}
} else {
// For fewer sub-queries, present each one with its question
for (const sq of decomposedQuery.subQueries) {
synthesized += `${sq.answer}\n\n`;
}
}
return synthesized.trim();
} catch (error: any) {
log.error(`Error synthesizing answer: ${error.message}`);
return "Error synthesizing the final answer.";
}
}
/**
* Generate a status report on the progress of answering a complex query
*
* @param decomposedQuery The decomposed query
* @returns A status report string
*/
getQueryStatus(decomposedQuery: DecomposedQuery): string {
const answeredCount = decomposedQuery.subQueries.filter(sq => sq.isAnswered).length;
const totalCount = decomposedQuery.subQueries.length;
let status = `Progress: ${answeredCount}/${totalCount} sub-queries answered\n\n`;
for (const sq of decomposedQuery.subQueries) {
status += `${sq.isAnswered ? '✓' : '○'} ${sq.text}\n`;
if (sq.isAnswered) {
status += ` Answer: ${this.truncateText(sq.answer || "", 100)}\n`;
}
}
return status;
}
/**
* Assess the complexity of a query on a scale of 1-10
* This helps determine how many sub-queries are needed
*
* @param query The query to assess
* @returns A complexity score from 1-10
*/
assessQueryComplexity(query: string): number {
// Count the number of question marks as a basic indicator
const questionMarkCount = (query.match(/\?/g) || []).length;
// Count potential sub-questions based on question words
const questionWords = ['what', 'how', 'why', 'where', 'when', 'who', 'which'];
const questionWordMatches = questionWords.map(word => {
const regex = new RegExp(`\\b${word}\\b`, 'gi');
return (query.match(regex) || []).length;
});
const questionWordCount = questionWordMatches.reduce((sum, count) => sum + count, 0);
// Look for conjunctions which might join multiple questions
const conjunctionCount = (query.match(/\b(and|or|but|as well as)\b/gi) || []).length;
// Look for complex requirements
const comparisonCount = (query.match(/\b(compare|versus|vs|difference|similarities?)\b/gi) || []).length;
const analysisCount = (query.match(/\b(analyze|examine|investigate|explore|explain|discuss)\b/gi) || []).length;
// Calculate base complexity
let complexity = 1;
// Add for multiple questions
complexity += Math.min(2, questionMarkCount);
// Add for question words beyond the first one
complexity += Math.min(2, Math.max(0, questionWordCount - 1));
// Add for conjunctions that might join questions
complexity += Math.min(2, conjunctionCount);
// Add for comparative/analytical requirements
complexity += Math.min(2, comparisonCount + analysisCount);
// Add for overall length/complexity
if (query.length > 100) complexity += 1;
if (query.length > 200) complexity += 1;
// Ensure we stay in the 1-10 range
return Math.max(1, Math.min(10, complexity));
}
/**
* Generate a unique ID for a sub-query
*/
private generateSubQueryId(): string {
return `sq_${Date.now()}_${QueryDecompositionTool.queryCounter++}`;
}
/**
* Create sub-queries based on the original query and optional context
*/
private createSubQueries(query: string, context?: string): SubQuery[] {
const subQueries: SubQuery[] = [];
// 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({ subQueries.push({
id: this.generateSubQueryId(), id: this.generateSubQueryId(),
text: `What are the key characteristics of ${item1}?`, text: `What key information in the current note relates to: "${query}"?`,
reason: `Need to understand ${item1} for the comparison`, reason: 'Identifying directly relevant information in the current context',
isAnswered: false isAnswered: false
}); });
}
subQueries.push({ // 1. Look for multiple question marks
id: this.generateSubQueryId(), const questionSplit = query.split(/\?/).filter(q => q.trim().length > 0);
text: `What are the key characteristics of ${item2}?`,
reason: `Need to understand ${item2} for the comparison`,
isAnswered: false
});
subQueries.push({ if (questionSplit.length > 1) {
id: this.generateSubQueryId(), // Multiple distinct questions detected
text: `What are the main differences between ${item1} and ${item2}?`, for (let i = 0; i < questionSplit.length; i++) {
reason: 'Understanding key differences', const text = questionSplit[i].trim() + '?';
isAnswered: false subQueries.push({
}); id: this.generateSubQueryId(),
text,
reason: `Separate question ${i + 1} detected in the original query`,
isAnswered: false
});
}
// Also add a synthesis question
subQueries.push({ subQueries.push({
id: this.generateSubQueryId(), id: this.generateSubQueryId(),
text: `What are the main similarities between ${item1} and ${item2}?`, text: `How do the answers to these questions relate to each other in the context of the original query?`,
reason: 'Understanding key similarities', reason: 'Synthesizing information from multiple questions',
isAnswered: false 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
}); });
return subQueries; return subQueries;
}
} }
}
}
// 3. For complex questions without clear separation, create topic-based sub-queries // 2. Look for "and", "or", etc. connecting potentially separate questions
// Lowered the threshold to process more queries this way const conjunctions = [
if (query.length > 50) { { regex: /\b(compare|versus|vs\.?|difference between|similarities between)\b/i, label: 'comparison' },
// Extract potential key topics from the query { regex: /\b(list|enumerate)\b/i, label: 'listing' },
const words = query.toLowerCase().split(/\W+/).filter(w => { regex: /\b(analyze|examine|investigate|explore)\b/i, label: 'analysis' },
w.length > 3 && { regex: /\b(explain|why)\b/i, label: 'explanation' },
!['what', 'when', 'where', 'which', 'with', 'would', 'could', 'should', 'have', 'this', 'that', 'there', 'their'].includes(w) { regex: /\b(how to|steps to|process of)\b/i, label: 'procedure' }
); ];
// Count word frequencies // Check for comparison queries - these often need multiple sub-queries
const wordFrequency: Record<string, number> = {}; for (const conj of conjunctions) {
for (const word of words) { if (conj.regex.test(query)) {
wordFrequency[word] = (wordFrequency[word] || 0) + 1; if (conj.label === 'comparison') {
} // For comparisons, we need to research each item, then compare them
const comparisonMatch = query.match(/\b(compare|versus|vs\.?|difference between|similarities between)\s+(.+?)\s+(and|with|to)\s+(.+?)(\?|$)/i);
// Get top frequent words if (comparisonMatch) {
const topWords = Object.entries(wordFrequency) const item1 = comparisonMatch[2].trim();
.sort((a, b) => b[1] - a[1]) const item2 = comparisonMatch[4].trim();
.slice(0, 4) // Increased from 3 to 4
.map(entry => entry[0]);
if (topWords.length > 0) { subQueries.push({
// Create factual sub-query id: this.generateSubQueryId(),
subQueries.push({ text: `What are the key characteristics of ${item1}?`,
id: this.generateSubQueryId(), reason: `Need to understand ${item1} for the comparison`,
text: `What are the key facts about ${topWords.join(' and ')} relevant to this question?`, isAnswered: false
reason: 'Gathering basic information about main topics', });
isAnswered: false
});
// Add individual queries for each key topic subQueries.push({
topWords.forEach(word => { id: this.generateSubQueryId(),
subQueries.push({ text: `What are the key characteristics of ${item2}?`,
id: this.generateSubQueryId(), reason: `Need to understand ${item2} for the comparison`,
text: `What specific details about "${word}" are most relevant to the query?`, isAnswered: false
reason: `Detailed exploration of the "${word}" concept`, });
isAnswered: false
});
});
// Create relationship sub-query if multiple top words subQueries.push({
if (topWords.length > 1) { id: this.generateSubQueryId(),
for (let i = 0; i < topWords.length; i++) { text: `What are the main differences between ${item1} and ${item2}?`,
for (let j = i + 1; j < topWords.length; j++) { reason: 'Understanding key differences',
subQueries.push({ isAnswered: false
id: this.generateSubQueryId(), });
text: `How do ${topWords[i]} and ${topWords[j]} relate to each other?`,
reason: `Understanding relationship between ${topWords[i]} and ${topWords[j]}`, subQueries.push({
isAnswered: false 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
});
return subQueries;
}
}
} }
}
} }
// Add a "what else" query to ensure comprehensive coverage // 3. For complex questions without clear separation, create topic-based sub-queries
// 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 &&
!['what', 'when', 'where', 'which', 'with', 'would', 'could', 'should', 'have', 'this', 'that', 'there', 'their'].includes(w)
);
// Count word frequencies
const wordFrequency: Record<string, number> = {};
for (const word of words) {
wordFrequency[word] = (wordFrequency[word] || 0) + 1;
}
// Get top frequent words
const topWords = Object.entries(wordFrequency)
.sort((a, b) => b[1] - a[1])
.slice(0, 4) // Increased from 3 to 4
.map(entry => entry[0]);
if (topWords.length > 0) {
// Create factual sub-query
subQueries.push({
id: this.generateSubQueryId(),
text: `What are the key facts about ${topWords.join(' and ')} relevant to this question?`,
reason: 'Gathering basic information about main topics',
isAnswered: false
});
// Add individual queries for each key topic
topWords.forEach(word => {
subQueries.push({
id: this.generateSubQueryId(),
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(),
text: query,
reason: 'Original question to be answered after gathering information',
isAnswered: false
});
return subQueries;
}
}
// 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: `What other important aspects should be considered about this topic that might not be immediately obvious?`, text: query,
reason: 'Exploring non-obvious but relevant information', reason: 'Primary question',
isAnswered: false isAnswered: false
}); });
// Add the original query as the final synthesizing question // Add generic exploration questions even for "simple" queries
subQueries.push({ subQueries.push({
id: this.generateSubQueryId(), id: this.generateSubQueryId(),
text: query, text: `What background information is helpful to understand this query better?`,
reason: 'Original question to be answered after gathering information', reason: 'Gathering background context',
isAnswered: false isAnswered: false
});
subQueries.push({
id: this.generateSubQueryId(),
text: `What related concepts might be important to consider?`,
reason: 'Exploring related concepts',
isAnswered: false
}); });
return subQueries; return subQueries;
}
} }
// Fallback: If we can't meaningfully decompose, just use the original query /**
// But also add some generic exploration questions * Truncate text to a maximum length with ellipsis
subQueries.push({ */
id: this.generateSubQueryId(), private truncateText(text: string, maxLength: number): string {
text: query, if (text.length <= maxLength) return text;
reason: 'Primary question', return text.substring(0, maxLength - 3) + '...';
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
});
return subQueries;
}
/**
* Truncate text to a maximum length with ellipsis
*/
private truncateText(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength - 3) + '...';
}
} }
export default QueryDecompositionTool; export default QueryDecompositionTool;

View File

@ -147,22 +147,24 @@ export class ContextService {
// Step 4: Add agent tools context with thinking process if requested // Step 4: Add agent tools context with thinking process if requested
let enhancedContext = context; let enhancedContext = context;
if (contextNoteId) { try {
try { // Pass 'root' as the default noteId when no specific note is selected
const agentContext = await this.getAgentToolsContext( const noteIdToUse = contextNoteId || 'root';
contextNoteId, log.info(`Calling getAgentToolsContext with noteId=${noteIdToUse}, showThinking=${showThinking}`);
userQuestion,
showThinking,
relevantNotes
);
if (agentContext) { const agentContext = await this.getAgentToolsContext(
enhancedContext = enhancedContext + "\n\n" + agentContext; noteIdToUse,
} userQuestion,
} catch (error) { showThinking,
log.error(`Error getting agent tools context: ${error}`); relevantNotes
// Continue with the basic context );
if (agentContext) {
enhancedContext = enhancedContext + "\n\n" + agentContext;
} }
} catch (error) {
log.error(`Error getting agent tools context: ${error}`);
// Continue with the basic context
} }
return { return {
@ -402,8 +404,13 @@ export class ContextService {
// Add thinking process if requested // Add thinking process if requested
if (showThinking) { if (showThinking) {
log.info(`Including thinking process in context (showThinking=true)`);
agentContext += `\n## Reasoning Process\n`; agentContext += `\n## Reasoning Process\n`;
agentContext += contextualThinkingTool.getThinkingSummary(thinkingId); const thinkingSummary = contextualThinkingTool.getThinkingSummary(thinkingId);
log.info(`Thinking summary length: ${thinkingSummary.length} characters`);
agentContext += thinkingSummary;
} else {
log.info(`Skipping thinking process in context (showThinking=false)`);
} }
// Log stats about the context // Log stats about the context