mirror of https://github.com/TriliumNext/Notes
tool calling is close to working
getting closer to calling tools... we definitely need this closer to tool execution... agentic tool calling is...kind of working?pull/1325/head
parent
eb353df010
commit
26b1b08129
@ -0,0 +1,216 @@
|
|||||||
|
import { BasePipelineStage } from '../pipeline_stage.js';
|
||||||
|
import type { ToolExecutionInput } from '../interfaces.js';
|
||||||
|
import log from '../../../log.js';
|
||||||
|
import type { ChatResponse, Message } from '../../ai_interface.js';
|
||||||
|
import toolRegistry from '../../tools/tool_registry.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pipeline stage for handling LLM tool calling
|
||||||
|
* This stage is responsible for:
|
||||||
|
* 1. Detecting tool calls in LLM responses
|
||||||
|
* 2. Executing the appropriate tools
|
||||||
|
* 3. Adding tool results back to the conversation
|
||||||
|
* 4. Determining if we need to make another call to the LLM
|
||||||
|
*/
|
||||||
|
export class ToolCallingStage extends BasePipelineStage<ToolExecutionInput, { response: ChatResponse, needsFollowUp: boolean, messages: Message[] }> {
|
||||||
|
constructor() {
|
||||||
|
super('ToolCalling');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the LLM response and execute any tool calls
|
||||||
|
*/
|
||||||
|
protected async process(input: ToolExecutionInput): Promise<{ response: ChatResponse, needsFollowUp: boolean, messages: Message[] }> {
|
||||||
|
const { response, messages, options } = input;
|
||||||
|
|
||||||
|
// Check if the response has tool calls
|
||||||
|
if (!response.tool_calls || response.tool_calls.length === 0) {
|
||||||
|
// No tool calls, return original response and messages
|
||||||
|
log.info(`No tool calls detected in response from provider: ${response.provider}`);
|
||||||
|
return { response, needsFollowUp: false, messages };
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`LLM requested ${response.tool_calls.length} tool calls from provider: ${response.provider}`);
|
||||||
|
|
||||||
|
// Log response details for debugging
|
||||||
|
if (response.text) {
|
||||||
|
log.info(`Response text: "${response.text.substring(0, 200)}${response.text.length > 200 ? '...' : ''}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the registry has any tools
|
||||||
|
const availableTools = toolRegistry.getAllTools();
|
||||||
|
log.info(`Available tools in registry: ${availableTools.length}`);
|
||||||
|
|
||||||
|
if (availableTools.length === 0) {
|
||||||
|
log.error(`No tools available in registry, cannot execute tool calls`);
|
||||||
|
// Try to initialize tools as a recovery step
|
||||||
|
try {
|
||||||
|
log.info('Attempting to initialize tools as recovery step');
|
||||||
|
const toolInitializer = await import('../../tools/tool_initializer.js');
|
||||||
|
await toolInitializer.default.initializeTools();
|
||||||
|
log.info(`After recovery initialization: ${toolRegistry.getAllTools().length} tools available`);
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Failed to initialize tools in recovery step: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a copy of messages to add the assistant message with tool calls
|
||||||
|
const updatedMessages = [...messages];
|
||||||
|
|
||||||
|
// Add the assistant message with the tool calls
|
||||||
|
updatedMessages.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: response.text || "",
|
||||||
|
tool_calls: response.tool_calls
|
||||||
|
});
|
||||||
|
|
||||||
|
// Execute each tool call and add results to messages
|
||||||
|
const toolResults = await Promise.all(response.tool_calls.map(async (toolCall) => {
|
||||||
|
try {
|
||||||
|
log.info(`Tool call received - Name: ${toolCall.function.name}, ID: ${toolCall.id || 'unknown'}`);
|
||||||
|
|
||||||
|
// Log parameters
|
||||||
|
const argsStr = typeof toolCall.function.arguments === 'string'
|
||||||
|
? toolCall.function.arguments
|
||||||
|
: JSON.stringify(toolCall.function.arguments);
|
||||||
|
log.info(`Tool parameters: ${argsStr}`);
|
||||||
|
|
||||||
|
// Get the tool from registry
|
||||||
|
const tool = toolRegistry.getTool(toolCall.function.name);
|
||||||
|
|
||||||
|
if (!tool) {
|
||||||
|
throw new Error(`Tool not found: ${toolCall.function.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse arguments (handle both string and object formats)
|
||||||
|
let args;
|
||||||
|
// At this stage, arguments should already be processed by the provider-specific service
|
||||||
|
// But we still need to handle different formats just in case
|
||||||
|
if (typeof toolCall.function.arguments === 'string') {
|
||||||
|
log.info(`Received string arguments in tool calling stage: ${toolCall.function.arguments.substring(0, 50)}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to parse as JSON first
|
||||||
|
args = JSON.parse(toolCall.function.arguments);
|
||||||
|
log.info(`Parsed JSON arguments: ${Object.keys(args).join(', ')}`);
|
||||||
|
} catch (e) {
|
||||||
|
// If it's not valid JSON, try to check if it's a stringified object with quotes
|
||||||
|
log.info(`Failed to parse arguments as JSON, trying alternative parsing: ${e.message}`);
|
||||||
|
|
||||||
|
// Sometimes LLMs return stringified JSON with escaped quotes or incorrect quotes
|
||||||
|
// Try to clean it up
|
||||||
|
try {
|
||||||
|
const cleaned = toolCall.function.arguments
|
||||||
|
.replace(/^['"]|['"]$/g, '') // Remove surrounding quotes
|
||||||
|
.replace(/\\"/g, '"') // Replace escaped quotes
|
||||||
|
.replace(/([{,])\s*'([^']+)'\s*:/g, '$1"$2":') // Replace single quotes around property names
|
||||||
|
.replace(/([{,])\s*(\w+)\s*:/g, '$1"$2":'); // Add quotes around unquoted property names
|
||||||
|
|
||||||
|
log.info(`Cleaned argument string: ${cleaned}`);
|
||||||
|
args = JSON.parse(cleaned);
|
||||||
|
log.info(`Successfully parsed cleaned arguments: ${Object.keys(args).join(', ')}`);
|
||||||
|
} catch (cleanError) {
|
||||||
|
// If all parsing fails, treat it as a text argument
|
||||||
|
log.info(`Failed to parse cleaned arguments: ${cleanError.message}`);
|
||||||
|
args = { text: toolCall.function.arguments };
|
||||||
|
log.info(`Using text argument: ${args.text.substring(0, 50)}...`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Arguments are already an object
|
||||||
|
args = toolCall.function.arguments;
|
||||||
|
log.info(`Using object arguments with keys: ${Object.keys(args).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the tool
|
||||||
|
log.info(`================ EXECUTING TOOL: ${toolCall.function.name} ================`);
|
||||||
|
log.info(`Tool parameters: ${Object.keys(args).join(', ')}`);
|
||||||
|
log.info(`Parameters values: ${Object.entries(args).map(([k, v]) => `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`).join(', ')}`);
|
||||||
|
|
||||||
|
const executionStart = Date.now();
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
log.info(`Starting tool execution for ${toolCall.function.name}...`);
|
||||||
|
result = await tool.execute(args);
|
||||||
|
const executionTime = Date.now() - executionStart;
|
||||||
|
log.info(`================ TOOL EXECUTION COMPLETED in ${executionTime}ms ================`);
|
||||||
|
} catch (execError: any) {
|
||||||
|
const executionTime = Date.now() - executionStart;
|
||||||
|
log.error(`================ TOOL EXECUTION FAILED in ${executionTime}ms: ${execError.message} ================`);
|
||||||
|
throw execError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log execution result
|
||||||
|
const resultSummary = typeof result === 'string'
|
||||||
|
? `${result.substring(0, 100)}...`
|
||||||
|
: `Object with keys: ${Object.keys(result).join(', ')}`;
|
||||||
|
log.info(`Tool execution completed in ${executionTime}ms - Result: ${resultSummary}`);
|
||||||
|
|
||||||
|
// Return result with tool call ID
|
||||||
|
return {
|
||||||
|
toolCallId: toolCall.id,
|
||||||
|
name: toolCall.function.name,
|
||||||
|
result
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error executing tool ${toolCall.function.name}: ${error.message || String(error)}`);
|
||||||
|
|
||||||
|
// Return error message as result
|
||||||
|
return {
|
||||||
|
toolCallId: toolCall.id,
|
||||||
|
name: toolCall.function.name,
|
||||||
|
result: `Error: ${error.message || String(error)}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Add tool results as messages
|
||||||
|
toolResults.forEach(result => {
|
||||||
|
// Format the result content based on type
|
||||||
|
let content: string;
|
||||||
|
|
||||||
|
if (typeof result.result === 'string') {
|
||||||
|
content = result.result;
|
||||||
|
log.info(`Tool returned string result (${content.length} chars)`);
|
||||||
|
} else {
|
||||||
|
// For object results, format as JSON
|
||||||
|
try {
|
||||||
|
content = JSON.stringify(result.result, null, 2);
|
||||||
|
log.info(`Tool returned object result with keys: ${Object.keys(result.result).join(', ')}`);
|
||||||
|
} catch (error) {
|
||||||
|
content = String(result.result);
|
||||||
|
log.info(`Failed to stringify object result: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Adding tool result message - Tool: ${result.name}, ID: ${result.toolCallId || 'unknown'}, Length: ${content.length}`);
|
||||||
|
|
||||||
|
// Create a properly formatted tool response message
|
||||||
|
updatedMessages.push({
|
||||||
|
role: 'tool',
|
||||||
|
content: content,
|
||||||
|
name: result.name,
|
||||||
|
tool_call_id: result.toolCallId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log a sample of the content for debugging
|
||||||
|
const contentPreview = content.substring(0, 100) + (content.length > 100 ? '...' : '');
|
||||||
|
log.info(`Tool result preview: ${contentPreview}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info(`Added ${toolResults.length} tool results to conversation`);
|
||||||
|
|
||||||
|
// If we have tool results, we need a follow-up call to the LLM
|
||||||
|
const needsFollowUp = toolResults.length > 0;
|
||||||
|
|
||||||
|
if (needsFollowUp) {
|
||||||
|
log.info(`Tool execution complete, LLM follow-up required with ${updatedMessages.length} messages`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
response,
|
||||||
|
needsFollowUp,
|
||||||
|
messages: updatedMessages
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,206 @@
|
|||||||
|
import { BasePipelineStage } from '../pipeline_stage.js';
|
||||||
|
import type { VectorSearchInput } from '../interfaces.js';
|
||||||
|
import type { NoteSearchResult } from '../../interfaces/context_interfaces.js';
|
||||||
|
import log from '../../../log.js';
|
||||||
|
import queryEnhancer from '../../context/modules/query_enhancer.js';
|
||||||
|
import semanticSearch from '../../context/modules/semantic_search.js';
|
||||||
|
import aiServiceManager from '../../ai_service_manager.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pipeline stage for handling semantic vector search with query enhancement
|
||||||
|
* This centralizes all semantic search operations into the pipeline
|
||||||
|
*/
|
||||||
|
export class VectorSearchStage extends BasePipelineStage<VectorSearchInput, {
|
||||||
|
searchResults: NoteSearchResult[],
|
||||||
|
enhancedQueries?: string[]
|
||||||
|
}> {
|
||||||
|
constructor() {
|
||||||
|
super('VectorSearch');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute semantic search with optional query enhancement
|
||||||
|
*/
|
||||||
|
protected async process(input: VectorSearchInput): Promise<{
|
||||||
|
searchResults: NoteSearchResult[],
|
||||||
|
enhancedQueries?: string[]
|
||||||
|
}> {
|
||||||
|
const { query, noteId, options = {} } = input;
|
||||||
|
const {
|
||||||
|
maxResults = 10,
|
||||||
|
useEnhancedQueries = true,
|
||||||
|
threshold = 0.6,
|
||||||
|
llmService = null
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
log.info(`========== PIPELINE VECTOR SEARCH ==========`);
|
||||||
|
log.info(`Query: "${query.substring(0, 100)}${query.length > 100 ? '...' : ''}"`);
|
||||||
|
log.info(`Parameters: noteId=${noteId || 'global'}, maxResults=${maxResults}, useEnhancedQueries=${useEnhancedQueries}, threshold=${threshold}`);
|
||||||
|
log.info(`LLM Service provided: ${llmService ? 'yes' : 'no'}`);
|
||||||
|
log.info(`Start timestamp: ${new Date().toISOString()}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// STEP 1: Generate enhanced search queries if requested
|
||||||
|
let searchQueries: string[] = [query];
|
||||||
|
|
||||||
|
if (useEnhancedQueries) {
|
||||||
|
log.info(`PIPELINE VECTOR SEARCH: Generating enhanced queries for: "${query.substring(0, 50)}..."`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the LLM service to use for query enhancement
|
||||||
|
let enhancementService = llmService;
|
||||||
|
|
||||||
|
// If no service provided, use AI service manager to get the default service
|
||||||
|
if (!enhancementService) {
|
||||||
|
log.info(`No LLM service provided, using default from AI service manager`);
|
||||||
|
const manager = aiServiceManager.getInstance();
|
||||||
|
const provider = manager.getPreferredProvider();
|
||||||
|
enhancementService = manager.getService(provider);
|
||||||
|
log.info(`Using preferred provider "${provider}" with service type ${enhancementService.constructor.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a special service wrapper that prevents recursion
|
||||||
|
const recursionPreventionService = {
|
||||||
|
generateChatCompletion: async (messages: any, options: any) => {
|
||||||
|
// Add flags to prevent recursive calls
|
||||||
|
const safeOptions = {
|
||||||
|
...options,
|
||||||
|
bypassFormatter: true,
|
||||||
|
_bypassContextProcessing: true,
|
||||||
|
bypassQueryEnhancement: true, // Critical flag
|
||||||
|
directToolExecution: true,
|
||||||
|
enableTools: false // Disable tools for query enhancement
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use the actual service implementation but with safe options
|
||||||
|
return enhancementService.generateChatCompletion(messages, safeOptions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call the query enhancer with the safe service
|
||||||
|
searchQueries = await queryEnhancer.generateSearchQueries(query, recursionPreventionService);
|
||||||
|
log.info(`PIPELINE VECTOR SEARCH: Generated ${searchQueries.length} enhanced queries`);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`PIPELINE VECTOR SEARCH: Error generating search queries, using original: ${error}`);
|
||||||
|
searchQueries = [query]; // Fall back to original query
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.info(`PIPELINE VECTOR SEARCH: Using direct query without enhancement: "${query}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 2: Find relevant notes for each query
|
||||||
|
const allResults = new Map<string, NoteSearchResult>();
|
||||||
|
log.info(`PIPELINE VECTOR SEARCH: Searching for ${searchQueries.length} queries`);
|
||||||
|
|
||||||
|
for (const searchQuery of searchQueries) {
|
||||||
|
try {
|
||||||
|
log.info(`PIPELINE VECTOR SEARCH: Processing query: "${searchQuery.substring(0, 50)}..."`);
|
||||||
|
const results = await semanticSearch.findRelevantNotes(
|
||||||
|
searchQuery,
|
||||||
|
noteId || null,
|
||||||
|
maxResults
|
||||||
|
);
|
||||||
|
|
||||||
|
log.info(`PIPELINE VECTOR SEARCH: Found ${results.length} results for query "${searchQuery.substring(0, 50)}..."`);
|
||||||
|
|
||||||
|
// Combine results, avoiding duplicates and keeping the highest similarity score
|
||||||
|
for (const result of results) {
|
||||||
|
if (!allResults.has(result.noteId)) {
|
||||||
|
allResults.set(result.noteId, result);
|
||||||
|
} else {
|
||||||
|
// If note already exists, update similarity to max of both values
|
||||||
|
const existing = allResults.get(result.noteId);
|
||||||
|
if (existing && result.similarity > existing.similarity) {
|
||||||
|
existing.similarity = result.similarity;
|
||||||
|
allResults.set(result.noteId, existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`PIPELINE VECTOR SEARCH: Error searching for query "${searchQuery}": ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 3: Convert to array, filter and sort
|
||||||
|
const filteredResults = Array.from(allResults.values())
|
||||||
|
.filter(note => {
|
||||||
|
// Filter out notes with no content or very minimal content
|
||||||
|
const hasContent = note.content && note.content.trim().length > 10;
|
||||||
|
// Apply similarity threshold
|
||||||
|
const meetsThreshold = note.similarity >= threshold;
|
||||||
|
|
||||||
|
if (!hasContent) {
|
||||||
|
log.info(`PIPELINE VECTOR SEARCH: Filtering out empty/minimal note: "${note.title}" (${note.noteId})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!meetsThreshold) {
|
||||||
|
log.info(`PIPELINE VECTOR SEARCH: Filtering out low similarity note: "${note.title}" - ${Math.round(note.similarity * 100)}% < ${Math.round(threshold * 100)}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasContent && meetsThreshold;
|
||||||
|
})
|
||||||
|
.sort((a, b) => b.similarity - a.similarity)
|
||||||
|
.slice(0, maxResults);
|
||||||
|
|
||||||
|
log.info(`PIPELINE VECTOR SEARCH: Search complete, returning ${filteredResults.length} results after filtering`);
|
||||||
|
|
||||||
|
// Log top results in detail
|
||||||
|
if (filteredResults.length > 0) {
|
||||||
|
log.info(`========== VECTOR SEARCH RESULTS ==========`);
|
||||||
|
log.info(`Found ${filteredResults.length} relevant notes after filtering`);
|
||||||
|
|
||||||
|
const topResults = filteredResults.slice(0, 5); // Show top 5 for better diagnostics
|
||||||
|
topResults.forEach((result, idx) => {
|
||||||
|
log.info(`Result ${idx+1}:`);
|
||||||
|
log.info(` Title: "${result.title}"`);
|
||||||
|
log.info(` NoteID: ${result.noteId}`);
|
||||||
|
log.info(` Similarity: ${Math.round(result.similarity * 100)}%`);
|
||||||
|
|
||||||
|
if (result.content) {
|
||||||
|
const contentPreview = result.content.length > 150
|
||||||
|
? `${result.content.substring(0, 150)}...`
|
||||||
|
: result.content;
|
||||||
|
log.info(` Content preview: ${contentPreview}`);
|
||||||
|
log.info(` Content length: ${result.content.length} chars`);
|
||||||
|
} else {
|
||||||
|
log.info(` Content: None or not loaded`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filteredResults.length > 5) {
|
||||||
|
log.info(`... and ${filteredResults.length - 5} more results not shown`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`========== END VECTOR SEARCH RESULTS ==========`);
|
||||||
|
} else {
|
||||||
|
log.info(`No results found that meet the similarity threshold of ${threshold}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log final statistics
|
||||||
|
log.info(`Vector search statistics:`);
|
||||||
|
log.info(` Original query: "${query.substring(0, 50)}${query.length > 50 ? '...' : ''}"`);
|
||||||
|
if (searchQueries.length > 1) {
|
||||||
|
log.info(` Enhanced with ${searchQueries.length} search queries`);
|
||||||
|
searchQueries.forEach((q, i) => {
|
||||||
|
if (i > 0) { // Skip the original query
|
||||||
|
log.info(` Query ${i}: "${q.substring(0, 50)}${q.length > 50 ? '...' : ''}"`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
log.info(` Final results: ${filteredResults.length} notes`);
|
||||||
|
log.info(` End timestamp: ${new Date().toISOString()}`);
|
||||||
|
log.info(`========== END PIPELINE VECTOR SEARCH ==========`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchResults: filteredResults,
|
||||||
|
enhancedQueries: useEnhancedQueries ? searchQueries : undefined
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`PIPELINE VECTOR SEARCH: Error in vector search stage: ${error.message || String(error)}`);
|
||||||
|
return {
|
||||||
|
searchResults: [],
|
||||||
|
enhancedQueries: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* Read Note Tool
|
||||||
|
*
|
||||||
|
* This tool allows the LLM to read the content of a specific note.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Tool, ToolHandler } from './tool_interfaces.js';
|
||||||
|
import log from '../../log.js';
|
||||||
|
import becca from '../../../becca/becca.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition of the read note tool
|
||||||
|
*/
|
||||||
|
export const readNoteToolDefinition: Tool = {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'read_note',
|
||||||
|
description: 'Read the content of a specific note by its ID',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
noteId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The ID of the note to read'
|
||||||
|
},
|
||||||
|
includeAttributes: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Whether to include note attributes in the response (default: false)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['noteId']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read note tool implementation
|
||||||
|
*/
|
||||||
|
export class ReadNoteTool implements ToolHandler {
|
||||||
|
public definition: Tool = readNoteToolDefinition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the read note tool
|
||||||
|
*/
|
||||||
|
public async execute(args: { noteId: string, includeAttributes?: boolean }): Promise<string | object> {
|
||||||
|
try {
|
||||||
|
const { noteId, includeAttributes = false } = args;
|
||||||
|
|
||||||
|
log.info(`Executing read_note tool - NoteID: "${noteId}", IncludeAttributes: ${includeAttributes}`);
|
||||||
|
|
||||||
|
// Get the note from becca
|
||||||
|
const note = becca.notes[noteId];
|
||||||
|
|
||||||
|
if (!note) {
|
||||||
|
log.info(`Note with ID ${noteId} not found - returning error`);
|
||||||
|
return `Error: Note with ID ${noteId} not found`;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Found note: "${note.title}" (Type: ${note.type})`);
|
||||||
|
|
||||||
|
// Get note content
|
||||||
|
const startTime = Date.now();
|
||||||
|
const content = await note.getContent();
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
log.info(`Retrieved note content in ${duration}ms, content length: ${content?.length || 0} chars`);
|
||||||
|
|
||||||
|
// Prepare the response
|
||||||
|
const response: any = {
|
||||||
|
noteId: note.noteId,
|
||||||
|
title: note.title,
|
||||||
|
type: note.type,
|
||||||
|
content: content || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
// Include attributes if requested
|
||||||
|
if (includeAttributes) {
|
||||||
|
const attributes = note.getOwnedAttributes();
|
||||||
|
log.info(`Including ${attributes.length} attributes in response`);
|
||||||
|
|
||||||
|
response.attributes = attributes.map(attr => ({
|
||||||
|
name: attr.name,
|
||||||
|
value: attr.value,
|
||||||
|
type: attr.type
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (attributes.length > 0) {
|
||||||
|
// Log some example attributes
|
||||||
|
attributes.slice(0, 3).forEach((attr, index) => {
|
||||||
|
log.info(`Attribute ${index + 1}: ${attr.name}=${attr.value} (${attr.type})`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error executing read_note tool: ${error.message || String(error)}`);
|
||||||
|
return `Error: ${error.message || String(error)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* Search Notes Tool
|
||||||
|
*
|
||||||
|
* This tool allows the LLM to search for notes using semantic search.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Tool, ToolHandler } from './tool_interfaces.js';
|
||||||
|
import log from '../../log.js';
|
||||||
|
import aiServiceManager from '../ai_service_manager.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition of the search notes tool
|
||||||
|
*/
|
||||||
|
export const searchNotesToolDefinition: Tool = {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'search_notes',
|
||||||
|
description: 'Search for notes in the database using semantic search. Returns notes most semantically related to the query.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
query: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The search query to find semantically related notes'
|
||||||
|
},
|
||||||
|
parentNoteId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Optional parent note ID to restrict search to a specific branch'
|
||||||
|
},
|
||||||
|
maxResults: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Maximum number of results to return (default: 5)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['query']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search notes tool implementation
|
||||||
|
*/
|
||||||
|
export class SearchNotesTool implements ToolHandler {
|
||||||
|
public definition: Tool = searchNotesToolDefinition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the search notes tool
|
||||||
|
*/
|
||||||
|
public async execute(args: { query: string, parentNoteId?: string, maxResults?: number }): Promise<string | object> {
|
||||||
|
try {
|
||||||
|
const { query, parentNoteId, maxResults = 5 } = args;
|
||||||
|
|
||||||
|
log.info(`Executing search_notes tool - Query: "${query}", ParentNoteId: ${parentNoteId || 'not specified'}, MaxResults: ${maxResults}`);
|
||||||
|
|
||||||
|
// Get the vector search tool from the AI service manager
|
||||||
|
const vectorSearchTool = aiServiceManager.getVectorSearchTool();
|
||||||
|
log.info(`Retrieved vector search tool from AI service manager`);
|
||||||
|
|
||||||
|
// Execute the search
|
||||||
|
log.info(`Performing semantic search for: "${query}"`);
|
||||||
|
const searchStartTime = Date.now();
|
||||||
|
const results = await vectorSearchTool.searchNotes(query, {
|
||||||
|
parentNoteId,
|
||||||
|
maxResults
|
||||||
|
});
|
||||||
|
const searchDuration = Date.now() - searchStartTime;
|
||||||
|
|
||||||
|
log.info(`Search completed in ${searchDuration}ms, found ${results.length} matching notes`);
|
||||||
|
|
||||||
|
if (results.length > 0) {
|
||||||
|
// Log top results
|
||||||
|
results.slice(0, 3).forEach((result, index) => {
|
||||||
|
log.info(`Result ${index + 1}: "${result.title}" (similarity: ${Math.round(result.similarity * 100)}%)`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log.info(`No matching notes found for query: "${query}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the results
|
||||||
|
return {
|
||||||
|
count: results.length,
|
||||||
|
results: results.map(result => ({
|
||||||
|
noteId: result.noteId,
|
||||||
|
title: result.title,
|
||||||
|
preview: result.contentPreview,
|
||||||
|
similarity: Math.round(result.similarity * 100) / 100,
|
||||||
|
parentId: result.parentId
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error executing search_notes tool: ${error.message || String(error)}`);
|
||||||
|
return `Error: ${error.message || String(error)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Tool Initializer
|
||||||
|
*
|
||||||
|
* This module initializes all available tools for the LLM to use.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import toolRegistry from './tool_registry.js';
|
||||||
|
import { SearchNotesTool } from './search_notes_tool.js';
|
||||||
|
import { ReadNoteTool } from './read_note_tool.js';
|
||||||
|
import log from '../../log.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize all tools for the LLM
|
||||||
|
*/
|
||||||
|
export async function initializeTools(): Promise<void> {
|
||||||
|
try {
|
||||||
|
log.info('Initializing LLM tools...');
|
||||||
|
|
||||||
|
// Register basic notes tools
|
||||||
|
toolRegistry.registerTool(new SearchNotesTool());
|
||||||
|
toolRegistry.registerTool(new ReadNoteTool());
|
||||||
|
|
||||||
|
// More tools can be registered here
|
||||||
|
|
||||||
|
// Log registered tools
|
||||||
|
const toolCount = toolRegistry.getAllTools().length;
|
||||||
|
log.info(`Successfully registered ${toolCount} LLM tools`);
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error initializing LLM tools: ${error.message || String(error)}`);
|
||||||
|
// Don't throw, just log the error to prevent breaking the pipeline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
initializeTools
|
||||||
|
};
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Tool Interfaces
|
||||||
|
*
|
||||||
|
* This file defines the interfaces for the LLM tool calling system.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for a tool definition to be sent to the LLM
|
||||||
|
*/
|
||||||
|
export interface Tool {
|
||||||
|
type: 'function';
|
||||||
|
function: {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
parameters: {
|
||||||
|
type: 'object';
|
||||||
|
properties: Record<string, ToolParameter>;
|
||||||
|
required: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for a tool parameter
|
||||||
|
*/
|
||||||
|
export interface ToolParameter {
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
enum?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for a tool call from the LLM
|
||||||
|
*/
|
||||||
|
export interface ToolCall {
|
||||||
|
id?: string;
|
||||||
|
type?: string;
|
||||||
|
function: {
|
||||||
|
name: string;
|
||||||
|
arguments: Record<string, any> | string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for a tool handler that executes a tool
|
||||||
|
*/
|
||||||
|
export interface ToolHandler {
|
||||||
|
/**
|
||||||
|
* Tool definition to be sent to the LLM
|
||||||
|
*/
|
||||||
|
definition: Tool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the tool with the given arguments
|
||||||
|
*/
|
||||||
|
execute(args: Record<string, any>): Promise<string | object>;
|
||||||
|
}
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* Tool Registry
|
||||||
|
*
|
||||||
|
* This file defines the registry for tools that can be called by LLMs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Tool, ToolHandler } from './tool_interfaces.js';
|
||||||
|
import log from '../../log.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry for tools that can be called by LLMs
|
||||||
|
*/
|
||||||
|
export class ToolRegistry {
|
||||||
|
private static instance: ToolRegistry;
|
||||||
|
private tools: Map<string, ToolHandler> = new Map();
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get singleton instance of the tool registry
|
||||||
|
*/
|
||||||
|
public static getInstance(): ToolRegistry {
|
||||||
|
if (!ToolRegistry.instance) {
|
||||||
|
ToolRegistry.instance = new ToolRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ToolRegistry.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a tool with the registry
|
||||||
|
*/
|
||||||
|
public registerTool(handler: ToolHandler): void {
|
||||||
|
const name = handler.definition.function.name;
|
||||||
|
|
||||||
|
if (this.tools.has(name)) {
|
||||||
|
log.info(`Tool '${name}' already registered, replacing...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tools.set(name, handler);
|
||||||
|
log.info(`Registered tool: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a tool by name
|
||||||
|
*/
|
||||||
|
public getTool(name: string): ToolHandler | undefined {
|
||||||
|
return this.tools.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all registered tools
|
||||||
|
*/
|
||||||
|
public getAllTools(): ToolHandler[] {
|
||||||
|
return Array.from(this.tools.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all tool definitions for sending to LLM
|
||||||
|
*/
|
||||||
|
public getAllToolDefinitions(): Tool[] {
|
||||||
|
const toolDefs = Array.from(this.tools.values()).map(handler => handler.definition);
|
||||||
|
return toolDefs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
const toolRegistry = ToolRegistry.getInstance();
|
||||||
|
export default toolRegistry;
|
||||||
Loading…
Reference in New Issue