mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-08-13 04:13:19 +08:00
add some more useful tools
CLOSER.... works?
This commit is contained in:
parent
26b1b08129
commit
7725b924e9
@ -34,6 +34,7 @@ export interface ChatCompletionOptions {
|
|||||||
stream?: boolean; // Whether to stream the response
|
stream?: boolean; // Whether to stream the response
|
||||||
enableTools?: boolean; // Whether to enable tool calling
|
enableTools?: boolean; // Whether to enable tool calling
|
||||||
tools?: any[]; // Tools to provide to the LLM
|
tools?: any[]; // Tools to provide to the LLM
|
||||||
|
useAdvancedContext?: boolean; // Whether to use advanced context enrichment
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatResponse {
|
export interface ChatResponse {
|
||||||
|
@ -1,9 +1,21 @@
|
|||||||
import type { Message, ChatCompletionOptions } from './ai_interface.js';
|
import type { Message, ChatCompletionOptions, ChatResponse } from './ai_interface.js';
|
||||||
import chatStorageService from './chat_storage_service.js';
|
import chatStorageService from './chat_storage_service.js';
|
||||||
import log from '../log.js';
|
import log from '../log.js';
|
||||||
import { CONTEXT_PROMPTS, ERROR_PROMPTS } from './constants/llm_prompt_constants.js';
|
import { CONTEXT_PROMPTS, ERROR_PROMPTS } from './constants/llm_prompt_constants.js';
|
||||||
import { ChatPipeline } from './pipeline/chat_pipeline.js';
|
import { ChatPipeline } from './pipeline/chat_pipeline.js';
|
||||||
import type { ChatPipelineConfig, StreamCallback } from './pipeline/interfaces.js';
|
import type { ChatPipelineConfig, StreamCallback } from './pipeline/interfaces.js';
|
||||||
|
import aiServiceManager from './ai_service_manager.js';
|
||||||
|
import type { ChatPipelineInput } from './pipeline/interfaces.js';
|
||||||
|
|
||||||
|
// Update the ChatCompletionOptions interface to include the missing properties
|
||||||
|
// TODO fix
|
||||||
|
declare module './ai_interface.js' {
|
||||||
|
interface ChatCompletionOptions {
|
||||||
|
pipeline?: string;
|
||||||
|
noteId?: string;
|
||||||
|
useAdvancedContext?: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChatSession {
|
export interface ChatSession {
|
||||||
id: string;
|
id: string;
|
||||||
@ -365,7 +377,7 @@ export class ChatService {
|
|||||||
const pipeline = this.getPipeline(pipelineType);
|
const pipeline = this.getPipeline(pipelineType);
|
||||||
return pipeline.getMetrics();
|
return pipeline.getMetrics();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset pipeline metrics
|
* Reset pipeline metrics
|
||||||
*/
|
*/
|
||||||
@ -398,8 +410,62 @@ export class ChatService {
|
|||||||
// Take first 30 chars if too long
|
// Take first 30 chars if too long
|
||||||
return firstLine.substring(0, 27) + '...';
|
return firstLine.substring(0, 27) + '...';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a chat completion with a sequence of messages
|
||||||
|
* @param messages Messages array to send to the AI provider
|
||||||
|
* @param options Chat completion options
|
||||||
|
*/
|
||||||
|
async generateChatCompletion(messages: Message[], options: ChatCompletionOptions = {}): Promise<ChatResponse> {
|
||||||
|
log.info(`========== CHAT SERVICE FLOW CHECK ==========`);
|
||||||
|
log.info(`Entered generateChatCompletion in ChatService`);
|
||||||
|
log.info(`Using pipeline for chat completion: ${this.getPipeline(options.pipeline).constructor.name}`);
|
||||||
|
log.info(`Tool support enabled: ${options.enableTools !== false}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get AI service
|
||||||
|
const service = await aiServiceManager.getService();
|
||||||
|
if (!service) {
|
||||||
|
throw new Error('No AI service available');
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Using AI service: ${service.getName()}`);
|
||||||
|
|
||||||
|
// Prepare query extraction
|
||||||
|
const lastUserMessage = [...messages].reverse().find(m => m.role === 'user');
|
||||||
|
const query = lastUserMessage ? lastUserMessage.content : undefined;
|
||||||
|
|
||||||
|
// For advanced context processing, use the pipeline
|
||||||
|
if (options.useAdvancedContext && query) {
|
||||||
|
log.info(`Using chat pipeline for advanced context with query: ${query.substring(0, 50)}...`);
|
||||||
|
|
||||||
|
// Create a pipeline input with the query and messages
|
||||||
|
const pipelineInput: ChatPipelineInput = {
|
||||||
|
messages,
|
||||||
|
options,
|
||||||
|
query,
|
||||||
|
noteId: options.noteId
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute the pipeline
|
||||||
|
const pipeline = this.getPipeline(options.pipeline);
|
||||||
|
const response = await pipeline.execute(pipelineInput);
|
||||||
|
log.info(`Pipeline execution complete, response contains tools: ${response.tool_calls ? 'yes' : 'no'}`);
|
||||||
|
if (response.tool_calls) {
|
||||||
|
log.info(`Tool calls in pipeline response: ${response.tool_calls.length}`);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not using advanced context, use direct service call
|
||||||
|
return await service.generateChatCompletion(messages, options);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error in generateChatCompletion:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Singleton instance
|
// Singleton instance
|
||||||
const chatService = new ChatService();
|
const chatService = new ChatService();
|
||||||
export default chatService;
|
export default chatService;
|
||||||
|
@ -19,6 +19,13 @@ export interface LLMServiceInterface {
|
|||||||
stream?: boolean;
|
stream?: boolean;
|
||||||
systemPrompt?: string;
|
systemPrompt?: string;
|
||||||
}): Promise<ChatResponse>;
|
}): Promise<ChatResponse>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate search queries by decomposing a complex query into simpler ones
|
||||||
|
* @param query The original user query to decompose
|
||||||
|
* @returns An array of decomposed search queries
|
||||||
|
*/
|
||||||
|
generateSearchQueries?(query: string): Promise<string[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { ChatPipelineInput, ChatPipelineConfig, PipelineMetrics, StreamCallback } from './interfaces.js';
|
import type { ChatPipelineInput, ChatPipelineConfig, PipelineMetrics, StreamCallback } from './interfaces.js';
|
||||||
import type { ChatResponse, StreamChunk } from '../ai_interface.js';
|
import type { ChatResponse, StreamChunk, Message } from '../ai_interface.js';
|
||||||
import { ContextExtractionStage } from './stages/context_extraction_stage.js';
|
import { ContextExtractionStage } from './stages/context_extraction_stage.js';
|
||||||
import { SemanticContextExtractionStage } from './stages/semantic_context_extraction_stage.js';
|
import { SemanticContextExtractionStage } from './stages/semantic_context_extraction_stage.js';
|
||||||
import { AgentToolsContextStage } from './stages/agent_tools_context_stage.js';
|
import { AgentToolsContextStage } from './stages/agent_tools_context_stage.js';
|
||||||
@ -12,6 +12,7 @@ import { VectorSearchStage } from './stages/vector_search_stage.js';
|
|||||||
import toolRegistry from '../tools/tool_registry.js';
|
import toolRegistry from '../tools/tool_registry.js';
|
||||||
import toolInitializer from '../tools/tool_initializer.js';
|
import toolInitializer from '../tools/tool_initializer.js';
|
||||||
import log from '../../log.js';
|
import log from '../../log.js';
|
||||||
|
import type { LLMServiceInterface } from '../interfaces/agent_tool_interfaces.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pipeline for managing the entire chat flow
|
* Pipeline for managing the entire chat flow
|
||||||
@ -80,6 +81,7 @@ export class ChatPipeline {
|
|||||||
* This is the main entry point that orchestrates all pipeline stages
|
* This is the main entry point that orchestrates all pipeline stages
|
||||||
*/
|
*/
|
||||||
async execute(input: ChatPipelineInput): Promise<ChatResponse> {
|
async execute(input: ChatPipelineInput): Promise<ChatResponse> {
|
||||||
|
log.info(`========== STARTING CHAT PIPELINE ==========`);
|
||||||
log.info(`Executing chat pipeline with ${input.messages.length} messages`);
|
log.info(`Executing chat pipeline with ${input.messages.length} messages`);
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
this.metrics.totalExecutions++;
|
this.metrics.totalExecutions++;
|
||||||
@ -113,89 +115,107 @@ export class ChatPipeline {
|
|||||||
|
|
||||||
// First, select the appropriate model based on query complexity and content length
|
// First, select the appropriate model based on query complexity and content length
|
||||||
const modelSelectionStartTime = Date.now();
|
const modelSelectionStartTime = Date.now();
|
||||||
|
log.info(`========== MODEL SELECTION ==========`);
|
||||||
const modelSelection = await this.stages.modelSelection.execute({
|
const modelSelection = await this.stages.modelSelection.execute({
|
||||||
options: input.options,
|
options: input.options,
|
||||||
query: input.query,
|
query: input.query,
|
||||||
contentLength
|
contentLength
|
||||||
});
|
});
|
||||||
this.updateStageMetrics('modelSelection', modelSelectionStartTime);
|
this.updateStageMetrics('modelSelection', modelSelectionStartTime);
|
||||||
|
log.info(`Selected model: ${modelSelection.options.model || 'default'}, enableTools: ${modelSelection.options.enableTools}`);
|
||||||
|
|
||||||
// Determine if we should use tools or semantic context
|
// Determine if we should use tools or semantic context
|
||||||
const useTools = modelSelection.options.enableTools === true;
|
const useTools = modelSelection.options.enableTools === true;
|
||||||
|
const useEnhancedContext = input.options?.useAdvancedContext === true;
|
||||||
|
|
||||||
// Determine which pipeline flow to use
|
// Early return if we don't have a query or enhanced context is disabled
|
||||||
let context: string | undefined;
|
if (!input.query || !useEnhancedContext) {
|
||||||
|
log.info(`========== SIMPLE QUERY MODE ==========`);
|
||||||
|
log.info('Enhanced context disabled or no query provided, skipping context enrichment');
|
||||||
|
|
||||||
// For context-aware chats, get the appropriate context
|
// Prepare messages without additional context
|
||||||
if (input.noteId && input.query) {
|
const messagePreparationStartTime = Date.now();
|
||||||
const contextStartTime = Date.now();
|
const preparedMessages = await this.stages.messagePreparation.execute({
|
||||||
if (input.showThinking) {
|
messages: input.messages,
|
||||||
// Get enhanced context with agent tools if thinking is enabled
|
systemPrompt: input.options?.systemPrompt,
|
||||||
const agentContext = await this.stages.agentToolsContext.execute({
|
options: modelSelection.options
|
||||||
noteId: input.noteId,
|
});
|
||||||
query: input.query,
|
this.updateStageMetrics('messagePreparation', messagePreparationStartTime);
|
||||||
showThinking: input.showThinking
|
|
||||||
});
|
// Generate completion using the LLM
|
||||||
context = agentContext.context;
|
const llmStartTime = Date.now();
|
||||||
this.updateStageMetrics('agentToolsContext', contextStartTime);
|
const completion = await this.stages.llmCompletion.execute({
|
||||||
} else if (!useTools) {
|
messages: preparedMessages.messages,
|
||||||
// Only get semantic context if tools are NOT enabled
|
options: modelSelection.options
|
||||||
// When tools are enabled, we'll let the LLM request context via tools instead
|
});
|
||||||
log.info('Getting semantic context for note using pipeline stages');
|
this.updateStageMetrics('llmCompletion', llmStartTime);
|
||||||
|
|
||||||
// First use the vector search stage to find relevant notes
|
return completion.response;
|
||||||
const vectorSearchStartTime = Date.now();
|
|
||||||
log.info(`Executing vector search stage for query: "${input.query?.substring(0, 50)}..."`);
|
|
||||||
|
|
||||||
const vectorSearchResult = await this.stages.vectorSearch.execute({
|
|
||||||
query: input.query || '',
|
|
||||||
noteId: input.noteId,
|
|
||||||
options: {
|
|
||||||
maxResults: 10,
|
|
||||||
useEnhancedQueries: true,
|
|
||||||
threshold: 0.6
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.updateStageMetrics('vectorSearch', vectorSearchStartTime);
|
|
||||||
|
|
||||||
log.info(`Vector search found ${vectorSearchResult.searchResults.length} relevant notes`);
|
|
||||||
|
|
||||||
// Then pass to the semantic context stage to build the formatted context
|
|
||||||
const semanticContext = await this.stages.semanticContextExtraction.execute({
|
|
||||||
noteId: input.noteId,
|
|
||||||
query: input.query,
|
|
||||||
messages: input.messages
|
|
||||||
});
|
|
||||||
|
|
||||||
context = semanticContext.context;
|
|
||||||
this.updateStageMetrics('semanticContextExtraction', contextStartTime);
|
|
||||||
} else {
|
|
||||||
log.info('Tools are enabled - using minimal direct context to avoid race conditions');
|
|
||||||
// Get context from current note directly without semantic search
|
|
||||||
if (input.noteId) {
|
|
||||||
try {
|
|
||||||
const contextExtractor = new (await import('../../llm/context/index.js')).ContextExtractor();
|
|
||||||
// Just get the direct content of the current note
|
|
||||||
context = await contextExtractor.extractContext(input.noteId, {
|
|
||||||
includeContent: true,
|
|
||||||
includeParents: true,
|
|
||||||
includeChildren: true,
|
|
||||||
includeLinks: true,
|
|
||||||
includeSimilar: false // Skip semantic search to avoid race conditions
|
|
||||||
});
|
|
||||||
log.info(`Direct context extracted (${context.length} chars) without semantic search`);
|
|
||||||
} catch (error: any) {
|
|
||||||
log.error(`Error extracting direct context: ${error.message}`);
|
|
||||||
context = ""; // Fallback to empty context if extraction fails
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
context = ""; // No note ID, so no context
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare messages with context and system prompt
|
// STAGE 1: Start with the user's query
|
||||||
|
const userQuery = input.query || '';
|
||||||
|
log.info(`========== STAGE 1: USER QUERY ==========`);
|
||||||
|
log.info(`Processing query with: question="${userQuery.substring(0, 50)}...", noteId=${input.noteId}, showThinking=${input.showThinking}`);
|
||||||
|
|
||||||
|
// STAGE 2: Perform query decomposition using the LLM
|
||||||
|
log.info(`========== STAGE 2: QUERY DECOMPOSITION ==========`);
|
||||||
|
log.info('Performing query decomposition to generate effective search queries');
|
||||||
|
const llmService = await this.getLLMService();
|
||||||
|
let searchQueries = [userQuery]; // Default to original query
|
||||||
|
|
||||||
|
if (llmService && llmService.generateSearchQueries) {
|
||||||
|
try {
|
||||||
|
const decompositionResult = await llmService.generateSearchQueries(userQuery);
|
||||||
|
if (decompositionResult && decompositionResult.length > 0) {
|
||||||
|
searchQueries = decompositionResult;
|
||||||
|
log.info(`Generated ${searchQueries.length} search queries: ${JSON.stringify(searchQueries)}`);
|
||||||
|
} else {
|
||||||
|
log.info('Query decomposition returned no results, using original query');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error in query decomposition: ${error.message || String(error)}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.info('No LLM service available for query decomposition, using original query');
|
||||||
|
}
|
||||||
|
|
||||||
|
// STAGE 3: Execute vector similarity search with decomposed queries
|
||||||
|
const vectorSearchStartTime = Date.now();
|
||||||
|
log.info(`========== STAGE 3: VECTOR SEARCH ==========`);
|
||||||
|
log.info('Using VectorSearchStage pipeline component to find relevant notes');
|
||||||
|
|
||||||
|
const vectorSearchResult = await this.stages.vectorSearch.execute({
|
||||||
|
query: userQuery,
|
||||||
|
noteId: input.noteId || 'global',
|
||||||
|
options: {
|
||||||
|
maxResults: 5, // Can be adjusted
|
||||||
|
useEnhancedQueries: true,
|
||||||
|
threshold: 0.6,
|
||||||
|
llmService: llmService || undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateStageMetrics('vectorSearch', vectorSearchStartTime);
|
||||||
|
|
||||||
|
log.info(`Vector search found ${vectorSearchResult.searchResults.length} relevant notes`);
|
||||||
|
|
||||||
|
// Extract context from search results
|
||||||
|
log.info(`========== SEMANTIC CONTEXT EXTRACTION ==========`);
|
||||||
|
const semanticContextStartTime = Date.now();
|
||||||
|
const semanticContext = await this.stages.semanticContextExtraction.execute({
|
||||||
|
noteId: input.noteId || 'global',
|
||||||
|
query: userQuery,
|
||||||
|
messages: input.messages,
|
||||||
|
searchResults: vectorSearchResult.searchResults
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = semanticContext.context;
|
||||||
|
this.updateStageMetrics('semanticContextExtraction', semanticContextStartTime);
|
||||||
|
log.info(`Extracted semantic context (${context.length} chars)`);
|
||||||
|
|
||||||
|
// STAGE 4: Prepare messages with context and tool definitions for the LLM
|
||||||
|
log.info(`========== STAGE 4: MESSAGE PREPARATION ==========`);
|
||||||
const messagePreparationStartTime = Date.now();
|
const messagePreparationStartTime = Date.now();
|
||||||
const preparedMessages = await this.stages.messagePreparation.execute({
|
const preparedMessages = await this.stages.messagePreparation.execute({
|
||||||
messages: input.messages,
|
messages: input.messages,
|
||||||
@ -204,9 +224,7 @@ export class ChatPipeline {
|
|||||||
options: modelSelection.options
|
options: modelSelection.options
|
||||||
});
|
});
|
||||||
this.updateStageMetrics('messagePreparation', messagePreparationStartTime);
|
this.updateStageMetrics('messagePreparation', messagePreparationStartTime);
|
||||||
|
log.info(`Prepared ${preparedMessages.messages.length} messages for LLM, tools enabled: ${useTools}`);
|
||||||
// Generate completion using the LLM
|
|
||||||
const llmStartTime = Date.now();
|
|
||||||
|
|
||||||
// Setup streaming handler if streaming is enabled and callback provided
|
// Setup streaming handler if streaming is enabled and callback provided
|
||||||
const enableStreaming = this.config.enableStreaming &&
|
const enableStreaming = this.config.enableStreaming &&
|
||||||
@ -218,11 +236,15 @@ export class ChatPipeline {
|
|||||||
modelSelection.options.stream = true;
|
modelSelection.options.stream = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// STAGE 5 & 6: Handle LLM completion and tool execution loop
|
||||||
|
log.info(`========== STAGE 5: LLM COMPLETION ==========`);
|
||||||
|
const llmStartTime = Date.now();
|
||||||
const completion = await this.stages.llmCompletion.execute({
|
const completion = await this.stages.llmCompletion.execute({
|
||||||
messages: preparedMessages.messages,
|
messages: preparedMessages.messages,
|
||||||
options: modelSelection.options
|
options: modelSelection.options
|
||||||
});
|
});
|
||||||
this.updateStageMetrics('llmCompletion', llmStartTime);
|
this.updateStageMetrics('llmCompletion', llmStartTime);
|
||||||
|
log.info(`Received LLM response from model: ${completion.response.model}, provider: ${completion.response.provider}`);
|
||||||
|
|
||||||
// Handle streaming if enabled and available
|
// Handle streaming if enabled and available
|
||||||
if (enableStreaming && completion.response.stream && streamCallback) {
|
if (enableStreaming && completion.response.stream && streamCallback) {
|
||||||
@ -242,123 +264,247 @@ export class ChatPipeline {
|
|||||||
// Process any tool calls in the response
|
// Process any tool calls in the response
|
||||||
let currentMessages = preparedMessages.messages;
|
let currentMessages = preparedMessages.messages;
|
||||||
let currentResponse = completion.response;
|
let currentResponse = completion.response;
|
||||||
let needsFollowUp = false;
|
|
||||||
let toolCallIterations = 0;
|
let toolCallIterations = 0;
|
||||||
const maxToolCallIterations = this.config.maxToolCallIterations;
|
const maxToolCallIterations = this.config.maxToolCallIterations;
|
||||||
|
|
||||||
// Check if tools were enabled in the options
|
// Check if tools were enabled in the options
|
||||||
const toolsEnabled = modelSelection.options.enableTools !== false;
|
const toolsEnabled = modelSelection.options.enableTools !== false;
|
||||||
|
|
||||||
log.info(`========== TOOL CALL PROCESSING ==========`);
|
// Log decision points for tool execution
|
||||||
log.info(`Tools enabled: ${toolsEnabled}`);
|
log.info(`========== TOOL EXECUTION DECISION ==========`);
|
||||||
log.info(`Tool calls in response: ${currentResponse.tool_calls ? currentResponse.tool_calls.length : 0}`);
|
log.info(`Tools enabled in options: ${toolsEnabled}`);
|
||||||
log.info(`Current response format: ${typeof currentResponse}`);
|
log.info(`Response provider: ${currentResponse.provider || 'unknown'}`);
|
||||||
log.info(`Response keys: ${Object.keys(currentResponse).join(', ')}`);
|
log.info(`Response model: ${currentResponse.model || 'unknown'}`);
|
||||||
|
log.info(`Response has tool_calls: ${currentResponse.tool_calls ? 'true' : 'false'}`);
|
||||||
// Detailed tool call inspection
|
|
||||||
if (currentResponse.tool_calls) {
|
if (currentResponse.tool_calls) {
|
||||||
currentResponse.tool_calls.forEach((tool, idx) => {
|
log.info(`Number of tool calls: ${currentResponse.tool_calls.length}`);
|
||||||
log.info(`Tool call ${idx+1}: ${JSON.stringify(tool)}`);
|
log.info(`Tool calls details: ${JSON.stringify(currentResponse.tool_calls)}`);
|
||||||
});
|
|
||||||
|
// Check if we have a response from Ollama, which might be handled differently
|
||||||
|
if (currentResponse.provider === 'Ollama') {
|
||||||
|
log.info(`ATTENTION: Response is from Ollama - checking if tool execution path is correct`);
|
||||||
|
log.info(`Tool calls type: ${typeof currentResponse.tool_calls}`);
|
||||||
|
log.info(`First tool call name: ${currentResponse.tool_calls[0]?.function?.name || 'unknown'}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process tool calls if present and tools are enabled
|
// Tool execution loop
|
||||||
if (toolsEnabled && currentResponse.tool_calls && currentResponse.tool_calls.length > 0) {
|
if (toolsEnabled && currentResponse.tool_calls && currentResponse.tool_calls.length > 0) {
|
||||||
|
log.info(`========== STAGE 6: TOOL EXECUTION ==========`);
|
||||||
log.info(`Response contains ${currentResponse.tool_calls.length} tool calls, processing...`);
|
log.info(`Response contains ${currentResponse.tool_calls.length} tool calls, processing...`);
|
||||||
|
|
||||||
// Start tool calling loop
|
// Format tool calls for logging
|
||||||
log.info(`Starting tool calling loop with max ${maxToolCallIterations} iterations`);
|
log.info(`========== TOOL CALL DETAILS ==========`);
|
||||||
|
currentResponse.tool_calls.forEach((toolCall, idx) => {
|
||||||
|
log.info(`Tool call ${idx + 1}: name=${toolCall.function?.name || 'unknown'}, id=${toolCall.id || 'no-id'}`);
|
||||||
|
log.info(`Arguments: ${toolCall.function?.arguments || '{}'}`);
|
||||||
|
});
|
||||||
|
|
||||||
do {
|
// Keep track of whether we're in a streaming response
|
||||||
log.info(`Tool calling iteration ${toolCallIterations + 1}`);
|
const isStreaming = enableStreaming && streamCallback;
|
||||||
|
let streamingPaused = false;
|
||||||
|
|
||||||
// Execute tool calling stage
|
// If streaming was enabled, send an update to the user
|
||||||
const toolCallingStartTime = Date.now();
|
if (isStreaming && streamCallback) {
|
||||||
const toolCallingResult = await this.stages.toolCalling.execute({
|
streamingPaused = true;
|
||||||
response: currentResponse,
|
await streamCallback('', true); // Signal pause in streaming
|
||||||
messages: currentMessages,
|
await streamCallback('\n\n[Executing tools...]\n\n', false);
|
||||||
options: modelSelection.options
|
}
|
||||||
});
|
|
||||||
this.updateStageMetrics('toolCalling', toolCallingStartTime);
|
|
||||||
|
|
||||||
// Update state for next iteration
|
while (toolCallIterations < maxToolCallIterations) {
|
||||||
currentMessages = toolCallingResult.messages;
|
toolCallIterations++;
|
||||||
needsFollowUp = toolCallingResult.needsFollowUp;
|
log.info(`========== TOOL ITERATION ${toolCallIterations}/${maxToolCallIterations} ==========`);
|
||||||
|
|
||||||
// Make another call to the LLM if needed
|
// Create a copy of messages before tool execution
|
||||||
if (needsFollowUp) {
|
const previousMessages = [...currentMessages];
|
||||||
log.info(`Tool execution completed, making follow-up LLM call (iteration ${toolCallIterations + 1})...`);
|
|
||||||
|
|
||||||
// Generate a new LLM response with the updated messages
|
try {
|
||||||
const followUpStartTime = Date.now();
|
const toolCallingStartTime = Date.now();
|
||||||
log.info(`Sending follow-up request to LLM with ${currentMessages.length} messages (including tool results)`);
|
log.info(`========== PIPELINE TOOL EXECUTION FLOW ==========`);
|
||||||
|
log.info(`About to call toolCalling.execute with ${currentResponse.tool_calls.length} tool calls`);
|
||||||
|
log.info(`Tool calls being passed to stage: ${JSON.stringify(currentResponse.tool_calls)}`);
|
||||||
|
|
||||||
const followUpCompletion = await this.stages.llmCompletion.execute({
|
const toolCallingResult = await this.stages.toolCalling.execute({
|
||||||
|
response: currentResponse,
|
||||||
messages: currentMessages,
|
messages: currentMessages,
|
||||||
options: modelSelection.options
|
options: modelSelection.options
|
||||||
});
|
});
|
||||||
this.updateStageMetrics('llmCompletion', followUpStartTime);
|
this.updateStageMetrics('toolCalling', toolCallingStartTime);
|
||||||
|
|
||||||
// Update current response for next iteration
|
log.info(`ToolCalling stage execution complete, got result with needsFollowUp: ${toolCallingResult.needsFollowUp}`);
|
||||||
currentResponse = followUpCompletion.response;
|
|
||||||
|
|
||||||
// Check for more tool calls
|
// Update messages with tool results
|
||||||
const hasMoreToolCalls = !!(currentResponse.tool_calls && currentResponse.tool_calls.length > 0);
|
currentMessages = toolCallingResult.messages;
|
||||||
|
|
||||||
if (hasMoreToolCalls) {
|
// Log the tool results for debugging
|
||||||
log.info(`Follow-up response contains ${currentResponse.tool_calls?.length || 0} more tool calls`);
|
const toolResultMessages = currentMessages.filter(
|
||||||
|
msg => msg.role === 'tool' && !previousMessages.includes(msg)
|
||||||
|
);
|
||||||
|
|
||||||
|
log.info(`========== TOOL EXECUTION RESULTS ==========`);
|
||||||
|
toolResultMessages.forEach((msg, idx) => {
|
||||||
|
log.info(`Tool result ${idx + 1}: tool_call_id=${msg.tool_call_id}, content=${msg.content.substring(0, 50)}...`);
|
||||||
|
|
||||||
|
// If streaming, show tool executions to the user
|
||||||
|
if (isStreaming && streamCallback) {
|
||||||
|
// For each tool result, format a readable message for the user
|
||||||
|
const toolName = this.getToolNameFromToolCallId(currentMessages, msg.tool_call_id || '');
|
||||||
|
const formattedToolResult = `[Tool: ${toolName || 'unknown'}]\n${msg.content}\n\n`;
|
||||||
|
streamCallback(formattedToolResult, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if we need another LLM completion for tool results
|
||||||
|
if (toolCallingResult.needsFollowUp) {
|
||||||
|
log.info(`========== TOOL FOLLOW-UP REQUIRED ==========`);
|
||||||
|
log.info('Tool execution complete, sending results back to LLM');
|
||||||
|
|
||||||
|
// Ensure messages are properly formatted
|
||||||
|
this.validateToolMessages(currentMessages);
|
||||||
|
|
||||||
|
// If streaming, show progress to the user
|
||||||
|
if (isStreaming && streamCallback) {
|
||||||
|
await streamCallback('[Generating response with tool results...]\n\n', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a new completion with the updated messages
|
||||||
|
const followUpStartTime = Date.now();
|
||||||
|
const followUpCompletion = await this.stages.llmCompletion.execute({
|
||||||
|
messages: currentMessages,
|
||||||
|
options: {
|
||||||
|
...modelSelection.options,
|
||||||
|
// Ensure tool support is still enabled for follow-up requests
|
||||||
|
enableTools: true,
|
||||||
|
// Disable streaming during tool execution follow-ups
|
||||||
|
stream: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.updateStageMetrics('llmCompletion', followUpStartTime);
|
||||||
|
|
||||||
|
// Update current response for the next iteration
|
||||||
|
currentResponse = followUpCompletion.response;
|
||||||
|
|
||||||
|
// Check if we need to continue the tool calling loop
|
||||||
|
if (!currentResponse.tool_calls || currentResponse.tool_calls.length === 0) {
|
||||||
|
log.info(`========== TOOL EXECUTION COMPLETE ==========`);
|
||||||
|
log.info('No more tool calls, breaking tool execution loop');
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
log.info(`========== ADDITIONAL TOOL CALLS DETECTED ==========`);
|
||||||
|
log.info(`Next iteration has ${currentResponse.tool_calls.length} more tool calls`);
|
||||||
|
// Log the next set of tool calls
|
||||||
|
currentResponse.tool_calls.forEach((toolCall, idx) => {
|
||||||
|
log.info(`Next tool call ${idx + 1}: name=${toolCall.function?.name || 'unknown'}, id=${toolCall.id || 'no-id'}`);
|
||||||
|
log.info(`Arguments: ${toolCall.function?.arguments || '{}'}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.info(`Follow-up response contains no more tool calls - completing tool loop`);
|
log.info(`========== TOOL EXECUTION COMPLETE ==========`);
|
||||||
|
log.info('No follow-up needed, breaking tool execution loop');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
log.info(`========== TOOL EXECUTION ERROR ==========`);
|
||||||
|
log.error(`Error in tool execution: ${error.message || String(error)}`);
|
||||||
|
|
||||||
|
// Add error message to the conversation if tool execution fails
|
||||||
|
currentMessages.push({
|
||||||
|
role: 'system',
|
||||||
|
content: `Error executing tool: ${error.message || String(error)}. Please try a different approach.`
|
||||||
|
});
|
||||||
|
|
||||||
|
// If streaming, show error to the user
|
||||||
|
if (isStreaming && streamCallback) {
|
||||||
|
await streamCallback(`[Tool execution error: ${error.message || 'unknown error'}]\n\n`, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue loop if there are more tool calls
|
// Make a follow-up request to the LLM with the error information
|
||||||
needsFollowUp = hasMoreToolCalls;
|
const errorFollowUpCompletion = await this.stages.llmCompletion.execute({
|
||||||
|
messages: currentMessages,
|
||||||
|
options: modelSelection.options
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update current response and break the tool loop
|
||||||
|
currentResponse = errorFollowUpCompletion.response;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment iteration counter
|
|
||||||
toolCallIterations++;
|
|
||||||
|
|
||||||
} while (needsFollowUp && toolCallIterations < maxToolCallIterations);
|
|
||||||
|
|
||||||
// If we hit max iterations but still have tool calls, log a warning
|
|
||||||
if (toolCallIterations >= maxToolCallIterations && needsFollowUp) {
|
|
||||||
log.error(`Reached maximum tool call iterations (${maxToolCallIterations}), stopping`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`Completed ${toolCallIterations} tool call iterations`);
|
if (toolCallIterations >= maxToolCallIterations) {
|
||||||
|
log.info(`========== MAXIMUM TOOL ITERATIONS REACHED ==========`);
|
||||||
|
log.error(`Reached maximum tool call iterations (${maxToolCallIterations}), terminating loop`);
|
||||||
|
|
||||||
|
// Add a message to inform the LLM that we've reached the limit
|
||||||
|
currentMessages.push({
|
||||||
|
role: 'system',
|
||||||
|
content: `Maximum tool call iterations (${maxToolCallIterations}) reached. Please provide your best response with the information gathered so far.`
|
||||||
|
});
|
||||||
|
|
||||||
|
// If streaming, inform the user about iteration limit
|
||||||
|
if (isStreaming && streamCallback) {
|
||||||
|
await streamCallback(`[Reached maximum of ${maxToolCallIterations} tool calls. Finalizing response...]\n\n`, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a final request to get a summary response
|
||||||
|
const finalFollowUpCompletion = await this.stages.llmCompletion.execute({
|
||||||
|
messages: currentMessages,
|
||||||
|
options: {
|
||||||
|
...modelSelection.options,
|
||||||
|
enableTools: false // Disable tools for the final response
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the current response
|
||||||
|
currentResponse = finalFollowUpCompletion.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If streaming was paused for tool execution, resume it now with the final response
|
||||||
|
if (isStreaming && streamCallback && streamingPaused) {
|
||||||
|
// Resume streaming with the final response text
|
||||||
|
await streamCallback(currentResponse.text, true);
|
||||||
|
}
|
||||||
|
} else if (toolsEnabled) {
|
||||||
|
log.info(`========== NO TOOL CALLS DETECTED ==========`);
|
||||||
|
log.info(`LLM response did not contain any tool calls, skipping tool execution`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For non-streaming responses, process the final response
|
// Process the final response
|
||||||
const processStartTime = Date.now();
|
log.info(`========== FINAL RESPONSE PROCESSING ==========`);
|
||||||
const processed = await this.stages.responseProcessing.execute({
|
const responseProcessingStartTime = Date.now();
|
||||||
|
const processedResponse = await this.stages.responseProcessing.execute({
|
||||||
response: currentResponse,
|
response: currentResponse,
|
||||||
options: input.options
|
options: modelSelection.options
|
||||||
});
|
});
|
||||||
this.updateStageMetrics('responseProcessing', processStartTime);
|
this.updateStageMetrics('responseProcessing', responseProcessingStartTime);
|
||||||
|
log.info(`Final response processed, returning to user (${processedResponse.text.length} chars)`);
|
||||||
|
|
||||||
// Combine response with processed text, using accumulated text if streamed
|
// Return the final response to the user
|
||||||
const finalResponse: ChatResponse = {
|
// The ResponseProcessingStage returns {text}, not {response}
|
||||||
...currentResponse,
|
// So we update our currentResponse with the processed text
|
||||||
text: accumulatedText || processed.text
|
currentResponse.text = processedResponse.text;
|
||||||
};
|
|
||||||
|
|
||||||
const endTime = Date.now();
|
log.info(`========== PIPELINE COMPLETE ==========`);
|
||||||
const executionTime = endTime - startTime;
|
return currentResponse;
|
||||||
|
|
||||||
// Update overall average execution time
|
|
||||||
this.metrics.averageExecutionTime =
|
|
||||||
(this.metrics.averageExecutionTime * (this.metrics.totalExecutions - 1) + executionTime) /
|
|
||||||
this.metrics.totalExecutions;
|
|
||||||
|
|
||||||
log.info(`Chat pipeline completed in ${executionTime}ms`);
|
|
||||||
|
|
||||||
return finalResponse;
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
log.error(`Error in chat pipeline: ${error.message}`);
|
log.info(`========== PIPELINE ERROR ==========`);
|
||||||
|
log.error(`Error in chat pipeline: ${error.message || String(error)}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to get an LLM service for query processing
|
||||||
|
*/
|
||||||
|
private async getLLMService(): Promise<LLMServiceInterface | null> {
|
||||||
|
try {
|
||||||
|
const aiServiceManager = await import('../ai_service_manager.js').then(module => module.default);
|
||||||
|
return aiServiceManager.getService();
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error getting LLM service: ${error.message || String(error)}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process a stream chunk through the response processing stage
|
* Process a stream chunk through the response processing stage
|
||||||
*/
|
*/
|
||||||
@ -428,4 +574,52 @@ export class ChatPipeline {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find tool name from tool call ID by looking at previous assistant messages
|
||||||
|
*/
|
||||||
|
private getToolNameFromToolCallId(messages: Message[], toolCallId: string): string {
|
||||||
|
if (!toolCallId) return 'unknown';
|
||||||
|
|
||||||
|
// Look for assistant messages with tool_calls
|
||||||
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
const message = messages[i];
|
||||||
|
if (message.role === 'assistant' && message.tool_calls) {
|
||||||
|
// Find the tool call with the matching ID
|
||||||
|
const toolCall = message.tool_calls.find(tc => tc.id === toolCallId);
|
||||||
|
if (toolCall && toolCall.function && toolCall.function.name) {
|
||||||
|
return toolCall.function.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate tool messages to ensure they're properly formatted
|
||||||
|
*/
|
||||||
|
private validateToolMessages(messages: Message[]): void {
|
||||||
|
for (let i = 0; i < messages.length; i++) {
|
||||||
|
const message = messages[i];
|
||||||
|
|
||||||
|
// Ensure tool messages have required fields
|
||||||
|
if (message.role === 'tool') {
|
||||||
|
if (!message.tool_call_id) {
|
||||||
|
log.info(`Tool message missing tool_call_id, adding placeholder`);
|
||||||
|
message.tool_call_id = `tool_${i}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content should be a string
|
||||||
|
if (typeof message.content !== 'string') {
|
||||||
|
log.info(`Tool message content is not a string, converting`);
|
||||||
|
try {
|
||||||
|
message.content = JSON.stringify(message.content);
|
||||||
|
} catch (e) {
|
||||||
|
message.content = String(message.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,25 +22,29 @@ export class ToolCallingStage extends BasePipelineStage<ToolExecutionInput, { re
|
|||||||
*/
|
*/
|
||||||
protected async process(input: ToolExecutionInput): Promise<{ response: ChatResponse, needsFollowUp: boolean, messages: Message[] }> {
|
protected async process(input: ToolExecutionInput): Promise<{ response: ChatResponse, needsFollowUp: boolean, messages: Message[] }> {
|
||||||
const { response, messages, options } = input;
|
const { response, messages, options } = input;
|
||||||
|
|
||||||
|
log.info(`========== TOOL CALLING STAGE ENTRY ==========`);
|
||||||
|
log.info(`Response provider: ${response.provider}, model: ${response.model || 'unknown'}`);
|
||||||
|
|
||||||
// Check if the response has tool calls
|
// Check if the response has tool calls
|
||||||
if (!response.tool_calls || response.tool_calls.length === 0) {
|
if (!response.tool_calls || response.tool_calls.length === 0) {
|
||||||
// No tool calls, return original response and messages
|
// No tool calls, return original response and messages
|
||||||
log.info(`No tool calls detected in response from provider: ${response.provider}`);
|
log.info(`No tool calls detected in response from provider: ${response.provider}`);
|
||||||
|
log.info(`===== EXITING TOOL CALLING STAGE: No tool_calls =====`);
|
||||||
return { response, needsFollowUp: false, messages };
|
return { response, needsFollowUp: false, messages };
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`LLM requested ${response.tool_calls.length} tool calls from provider: ${response.provider}`);
|
log.info(`LLM requested ${response.tool_calls.length} tool calls from provider: ${response.provider}`);
|
||||||
|
|
||||||
// Log response details for debugging
|
// Log response details for debugging
|
||||||
if (response.text) {
|
if (response.text) {
|
||||||
log.info(`Response text: "${response.text.substring(0, 200)}${response.text.length > 200 ? '...' : ''}"`);
|
log.info(`Response text: "${response.text.substring(0, 200)}${response.text.length > 200 ? '...' : ''}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the registry has any tools
|
// Check if the registry has any tools
|
||||||
const availableTools = toolRegistry.getAllTools();
|
const availableTools = toolRegistry.getAllTools();
|
||||||
log.info(`Available tools in registry: ${availableTools.length}`);
|
log.info(`Available tools in registry: ${availableTools.length}`);
|
||||||
|
|
||||||
if (availableTools.length === 0) {
|
if (availableTools.length === 0) {
|
||||||
log.error(`No tools available in registry, cannot execute tool calls`);
|
log.error(`No tools available in registry, cannot execute tool calls`);
|
||||||
// Try to initialize tools as a recovery step
|
// Try to initialize tools as a recovery step
|
||||||
@ -53,10 +57,10 @@ export class ToolCallingStage extends BasePipelineStage<ToolExecutionInput, { re
|
|||||||
log.error(`Failed to initialize tools in recovery step: ${error.message}`);
|
log.error(`Failed to initialize tools in recovery step: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a copy of messages to add the assistant message with tool calls
|
// Create a copy of messages to add the assistant message with tool calls
|
||||||
const updatedMessages = [...messages];
|
const updatedMessages = [...messages];
|
||||||
|
|
||||||
// Add the assistant message with the tool calls
|
// Add the assistant message with the tool calls
|
||||||
updatedMessages.push({
|
updatedMessages.push({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
@ -65,38 +69,44 @@ export class ToolCallingStage extends BasePipelineStage<ToolExecutionInput, { re
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Execute each tool call and add results to messages
|
// Execute each tool call and add results to messages
|
||||||
const toolResults = await Promise.all(response.tool_calls.map(async (toolCall) => {
|
log.info(`========== STARTING TOOL EXECUTION ==========`);
|
||||||
|
const toolResults = await Promise.all(response.tool_calls.map(async (toolCall, index) => {
|
||||||
try {
|
try {
|
||||||
log.info(`Tool call received - Name: ${toolCall.function.name}, ID: ${toolCall.id || 'unknown'}`);
|
log.info(`Tool call ${index + 1} received - Name: ${toolCall.function.name}, ID: ${toolCall.id || 'unknown'}`);
|
||||||
|
|
||||||
// Log parameters
|
// Log parameters
|
||||||
const argsStr = typeof toolCall.function.arguments === 'string'
|
const argsStr = typeof toolCall.function.arguments === 'string'
|
||||||
? toolCall.function.arguments
|
? toolCall.function.arguments
|
||||||
: JSON.stringify(toolCall.function.arguments);
|
: JSON.stringify(toolCall.function.arguments);
|
||||||
log.info(`Tool parameters: ${argsStr}`);
|
log.info(`Tool parameters: ${argsStr}`);
|
||||||
|
|
||||||
// Get the tool from registry
|
// Get the tool from registry
|
||||||
const tool = toolRegistry.getTool(toolCall.function.name);
|
const tool = toolRegistry.getTool(toolCall.function.name);
|
||||||
|
|
||||||
if (!tool) {
|
if (!tool) {
|
||||||
|
log.error(`Tool not found in registry: ${toolCall.function.name}`);
|
||||||
|
log.info(`Available tools: ${availableTools.map(t => t.definition.function.name).join(', ')}`);
|
||||||
throw new Error(`Tool not found: ${toolCall.function.name}`);
|
throw new Error(`Tool not found: ${toolCall.function.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info(`Tool found in registry: ${toolCall.function.name}`);
|
||||||
|
|
||||||
// Parse arguments (handle both string and object formats)
|
// Parse arguments (handle both string and object formats)
|
||||||
let args;
|
let args;
|
||||||
// At this stage, arguments should already be processed by the provider-specific service
|
// At this stage, arguments should already be processed by the provider-specific service
|
||||||
// But we still need to handle different formats just in case
|
// But we still need to handle different formats just in case
|
||||||
if (typeof toolCall.function.arguments === 'string') {
|
if (typeof toolCall.function.arguments === 'string') {
|
||||||
log.info(`Received string arguments in tool calling stage: ${toolCall.function.arguments.substring(0, 50)}...`);
|
log.info(`Received string arguments in tool calling stage: ${toolCall.function.arguments.substring(0, 50)}...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to parse as JSON first
|
// Try to parse as JSON first
|
||||||
args = JSON.parse(toolCall.function.arguments);
|
args = JSON.parse(toolCall.function.arguments);
|
||||||
log.info(`Parsed JSON arguments: ${Object.keys(args).join(', ')}`);
|
log.info(`Parsed JSON arguments: ${Object.keys(args).join(', ')}`);
|
||||||
} catch (e) {
|
} catch (e: unknown) {
|
||||||
// If it's not valid JSON, try to check if it's a stringified object with quotes
|
// 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}`);
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||||
|
log.info(`Failed to parse arguments as JSON, trying alternative parsing: ${errorMessage}`);
|
||||||
|
|
||||||
// Sometimes LLMs return stringified JSON with escaped quotes or incorrect quotes
|
// Sometimes LLMs return stringified JSON with escaped quotes or incorrect quotes
|
||||||
// Try to clean it up
|
// Try to clean it up
|
||||||
try {
|
try {
|
||||||
@ -105,13 +115,14 @@ export class ToolCallingStage extends BasePipelineStage<ToolExecutionInput, { re
|
|||||||
.replace(/\\"/g, '"') // Replace escaped quotes
|
.replace(/\\"/g, '"') // Replace escaped quotes
|
||||||
.replace(/([{,])\s*'([^']+)'\s*:/g, '$1"$2":') // Replace single quotes around property names
|
.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
|
.replace(/([{,])\s*(\w+)\s*:/g, '$1"$2":'); // Add quotes around unquoted property names
|
||||||
|
|
||||||
log.info(`Cleaned argument string: ${cleaned}`);
|
log.info(`Cleaned argument string: ${cleaned}`);
|
||||||
args = JSON.parse(cleaned);
|
args = JSON.parse(cleaned);
|
||||||
log.info(`Successfully parsed cleaned arguments: ${Object.keys(args).join(', ')}`);
|
log.info(`Successfully parsed cleaned arguments: ${Object.keys(args).join(', ')}`);
|
||||||
} catch (cleanError) {
|
} catch (cleanError: unknown) {
|
||||||
// If all parsing fails, treat it as a text argument
|
// If all parsing fails, treat it as a text argument
|
||||||
log.info(`Failed to parse cleaned arguments: ${cleanError.message}`);
|
const cleanErrorMessage = cleanError instanceof Error ? cleanError.message : String(cleanError);
|
||||||
|
log.info(`Failed to parse cleaned arguments: ${cleanErrorMessage}`);
|
||||||
args = { text: toolCall.function.arguments };
|
args = { text: toolCall.function.arguments };
|
||||||
log.info(`Using text argument: ${args.text.substring(0, 50)}...`);
|
log.info(`Using text argument: ${args.text.substring(0, 50)}...`);
|
||||||
}
|
}
|
||||||
@ -121,12 +132,12 @@ export class ToolCallingStage extends BasePipelineStage<ToolExecutionInput, { re
|
|||||||
args = toolCall.function.arguments;
|
args = toolCall.function.arguments;
|
||||||
log.info(`Using object arguments with keys: ${Object.keys(args).join(', ')}`);
|
log.info(`Using object arguments with keys: ${Object.keys(args).join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the tool
|
// Execute the tool
|
||||||
log.info(`================ EXECUTING TOOL: ${toolCall.function.name} ================`);
|
log.info(`================ EXECUTING TOOL: ${toolCall.function.name} ================`);
|
||||||
log.info(`Tool parameters: ${Object.keys(args).join(', ')}`);
|
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(', ')}`);
|
log.info(`Parameters values: ${Object.entries(args).map(([k, v]) => `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`).join(', ')}`);
|
||||||
|
|
||||||
const executionStart = Date.now();
|
const executionStart = Date.now();
|
||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
@ -139,13 +150,14 @@ export class ToolCallingStage extends BasePipelineStage<ToolExecutionInput, { re
|
|||||||
log.error(`================ TOOL EXECUTION FAILED in ${executionTime}ms: ${execError.message} ================`);
|
log.error(`================ TOOL EXECUTION FAILED in ${executionTime}ms: ${execError.message} ================`);
|
||||||
throw execError;
|
throw execError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log execution result
|
// Log execution result
|
||||||
const resultSummary = typeof result === 'string'
|
const resultSummary = typeof result === 'string'
|
||||||
? `${result.substring(0, 100)}...`
|
? `${result.substring(0, 100)}...`
|
||||||
: `Object with keys: ${Object.keys(result).join(', ')}`;
|
: `Object with keys: ${Object.keys(result).join(', ')}`;
|
||||||
|
const executionTime = Date.now() - executionStart;
|
||||||
log.info(`Tool execution completed in ${executionTime}ms - Result: ${resultSummary}`);
|
log.info(`Tool execution completed in ${executionTime}ms - Result: ${resultSummary}`);
|
||||||
|
|
||||||
// Return result with tool call ID
|
// Return result with tool call ID
|
||||||
return {
|
return {
|
||||||
toolCallId: toolCall.id,
|
toolCallId: toolCall.id,
|
||||||
@ -154,7 +166,7 @@ export class ToolCallingStage extends BasePipelineStage<ToolExecutionInput, { re
|
|||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
log.error(`Error executing tool ${toolCall.function.name}: ${error.message || String(error)}`);
|
log.error(`Error executing tool ${toolCall.function.name}: ${error.message || String(error)}`);
|
||||||
|
|
||||||
// Return error message as result
|
// Return error message as result
|
||||||
return {
|
return {
|
||||||
toolCallId: toolCall.id,
|
toolCallId: toolCall.id,
|
||||||
@ -163,12 +175,12 @@ export class ToolCallingStage extends BasePipelineStage<ToolExecutionInput, { re
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Add tool results as messages
|
// Add tool results as messages
|
||||||
toolResults.forEach(result => {
|
toolResults.forEach(result => {
|
||||||
// Format the result content based on type
|
// Format the result content based on type
|
||||||
let content: string;
|
let content: string;
|
||||||
|
|
||||||
if (typeof result.result === 'string') {
|
if (typeof result.result === 'string') {
|
||||||
content = result.result;
|
content = result.result;
|
||||||
log.info(`Tool returned string result (${content.length} chars)`);
|
log.info(`Tool returned string result (${content.length} chars)`);
|
||||||
@ -182,9 +194,9 @@ export class ToolCallingStage extends BasePipelineStage<ToolExecutionInput, { re
|
|||||||
log.info(`Failed to stringify object result: ${error}`);
|
log.info(`Failed to stringify object result: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`Adding tool result message - Tool: ${result.name}, ID: ${result.toolCallId || 'unknown'}, Length: ${content.length}`);
|
log.info(`Adding tool result message - Tool: ${result.name}, ID: ${result.toolCallId || 'unknown'}, Length: ${content.length}`);
|
||||||
|
|
||||||
// Create a properly formatted tool response message
|
// Create a properly formatted tool response message
|
||||||
updatedMessages.push({
|
updatedMessages.push({
|
||||||
role: 'tool',
|
role: 'tool',
|
||||||
@ -192,21 +204,21 @@ export class ToolCallingStage extends BasePipelineStage<ToolExecutionInput, { re
|
|||||||
name: result.name,
|
name: result.name,
|
||||||
tool_call_id: result.toolCallId
|
tool_call_id: result.toolCallId
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log a sample of the content for debugging
|
// Log a sample of the content for debugging
|
||||||
const contentPreview = content.substring(0, 100) + (content.length > 100 ? '...' : '');
|
const contentPreview = content.substring(0, 100) + (content.length > 100 ? '...' : '');
|
||||||
log.info(`Tool result preview: ${contentPreview}`);
|
log.info(`Tool result preview: ${contentPreview}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info(`Added ${toolResults.length} tool results to conversation`);
|
log.info(`Added ${toolResults.length} tool results to conversation`);
|
||||||
|
|
||||||
// If we have tool results, we need a follow-up call to the LLM
|
// If we have tool results, we need a follow-up call to the LLM
|
||||||
const needsFollowUp = toolResults.length > 0;
|
const needsFollowUp = toolResults.length > 0;
|
||||||
|
|
||||||
if (needsFollowUp) {
|
if (needsFollowUp) {
|
||||||
log.info(`Tool execution complete, LLM follow-up required with ${updatedMessages.length} messages`);
|
log.info(`Tool execution complete, LLM follow-up required with ${updatedMessages.length} messages`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
response,
|
response,
|
||||||
needsFollowUp,
|
needsFollowUp,
|
||||||
|
@ -107,14 +107,14 @@ export class OllamaService extends BaseAIService {
|
|||||||
const tools = toolRegistry.getAllToolDefinitions();
|
const tools = toolRegistry.getAllToolDefinitions();
|
||||||
requestBody.tools = tools;
|
requestBody.tools = tools;
|
||||||
log.info(`Adding ${tools.length} tools to request`);
|
log.info(`Adding ${tools.length} tools to request`);
|
||||||
|
|
||||||
// If no tools found, reinitialize
|
// If no tools found, reinitialize
|
||||||
if (tools.length === 0) {
|
if (tools.length === 0) {
|
||||||
log.info('No tools found in registry, re-initializing...');
|
log.info('No tools found in registry, re-initializing...');
|
||||||
try {
|
try {
|
||||||
const toolInitializer = await import('../tools/tool_initializer.js');
|
const toolInitializer = await import('../tools/tool_initializer.js');
|
||||||
await toolInitializer.default.initializeTools();
|
await toolInitializer.default.initializeTools();
|
||||||
|
|
||||||
// Try again
|
// Try again
|
||||||
requestBody.tools = toolRegistry.getAllToolDefinitions();
|
requestBody.tools = toolRegistry.getAllToolDefinitions();
|
||||||
log.info(`After re-initialization: ${requestBody.tools.length} tools available`);
|
log.info(`After re-initialization: ${requestBody.tools.length} tools available`);
|
||||||
@ -139,43 +139,43 @@ export class OllamaService extends BaseAIService {
|
|||||||
log.info(`========== OLLAMA API REQUEST ==========`);
|
log.info(`========== OLLAMA API REQUEST ==========`);
|
||||||
log.info(`Model: ${requestBody.model}, Messages: ${requestBody.messages.length}, Tools: ${requestBody.tools ? requestBody.tools.length : 0}`);
|
log.info(`Model: ${requestBody.model}, Messages: ${requestBody.messages.length}, Tools: ${requestBody.tools ? requestBody.tools.length : 0}`);
|
||||||
log.info(`Temperature: ${temperature}, Stream: ${requestBody.stream}, JSON response expected: ${expectsJsonResponse}`);
|
log.info(`Temperature: ${temperature}, Stream: ${requestBody.stream}, JSON response expected: ${expectsJsonResponse}`);
|
||||||
|
|
||||||
// Check message structure and log detailed information about each message
|
// Check message structure and log detailed information about each message
|
||||||
requestBody.messages.forEach((msg: any, index: number) => {
|
requestBody.messages.forEach((msg: any, index: number) => {
|
||||||
const keys = Object.keys(msg);
|
const keys = Object.keys(msg);
|
||||||
log.info(`Message ${index}, Role: ${msg.role}, Keys: ${keys.join(', ')}`);
|
log.info(`Message ${index}, Role: ${msg.role}, Keys: ${keys.join(', ')}`);
|
||||||
|
|
||||||
// Log message content preview
|
// Log message content preview
|
||||||
if (msg.content && typeof msg.content === 'string') {
|
if (msg.content && typeof msg.content === 'string') {
|
||||||
const contentPreview = msg.content.length > 200
|
const contentPreview = msg.content.length > 200
|
||||||
? `${msg.content.substring(0, 200)}...`
|
? `${msg.content.substring(0, 200)}...`
|
||||||
: msg.content;
|
: msg.content;
|
||||||
log.info(`Message ${index} content: ${contentPreview}`);
|
log.info(`Message ${index} content: ${contentPreview}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log tool-related details
|
// Log tool-related details
|
||||||
if (keys.includes('tool_calls')) {
|
if (keys.includes('tool_calls')) {
|
||||||
log.info(`Message ${index} has ${msg.tool_calls.length} tool calls:`);
|
log.info(`Message ${index} has ${msg.tool_calls.length} tool calls:`);
|
||||||
msg.tool_calls.forEach((call: any, callIdx: number) => {
|
msg.tool_calls.forEach((call: any, callIdx: number) => {
|
||||||
log.info(` Tool call ${callIdx}: ${call.function?.name || 'unknown'}, ID: ${call.id || 'unspecified'}`);
|
log.info(` Tool call ${callIdx}: ${call.function?.name || 'unknown'}, ID: ${call.id || 'unspecified'}`);
|
||||||
if (call.function?.arguments) {
|
if (call.function?.arguments) {
|
||||||
const argsPreview = typeof call.function.arguments === 'string'
|
const argsPreview = typeof call.function.arguments === 'string'
|
||||||
? call.function.arguments.substring(0, 100)
|
? call.function.arguments.substring(0, 100)
|
||||||
: JSON.stringify(call.function.arguments).substring(0, 100);
|
: JSON.stringify(call.function.arguments).substring(0, 100);
|
||||||
log.info(` Arguments: ${argsPreview}...`);
|
log.info(` Arguments: ${argsPreview}...`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keys.includes('tool_call_id')) {
|
if (keys.includes('tool_call_id')) {
|
||||||
log.info(`Message ${index} is a tool response for tool call ID: ${msg.tool_call_id}`);
|
log.info(`Message ${index} is a tool response for tool call ID: ${msg.tool_call_id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keys.includes('name') && msg.role === 'tool') {
|
if (keys.includes('name') && msg.role === 'tool') {
|
||||||
log.info(`Message ${index} is from tool: ${msg.name}`);
|
log.info(`Message ${index} is from tool: ${msg.name}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log tool definitions
|
// Log tool definitions
|
||||||
if (requestBody.tools && requestBody.tools.length > 0) {
|
if (requestBody.tools && requestBody.tools.length > 0) {
|
||||||
log.info(`Sending ${requestBody.tools.length} tool definitions:`);
|
log.info(`Sending ${requestBody.tools.length} tool definitions:`);
|
||||||
@ -185,19 +185,19 @@ export class OllamaService extends BaseAIService {
|
|||||||
log.info(` Description: ${tool.function.description.substring(0, 100)}...`);
|
log.info(` Description: ${tool.function.description.substring(0, 100)}...`);
|
||||||
}
|
}
|
||||||
if (tool.function?.parameters) {
|
if (tool.function?.parameters) {
|
||||||
const paramNames = tool.function.parameters.properties
|
const paramNames = tool.function.parameters.properties
|
||||||
? Object.keys(tool.function.parameters.properties)
|
? Object.keys(tool.function.parameters.properties)
|
||||||
: [];
|
: [];
|
||||||
log.info(` Parameters: ${paramNames.join(', ')}`);
|
log.info(` Parameters: ${paramNames.join(', ')}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log full request body (this will create large logs but is helpful for debugging)
|
// Log full request body (this will create large logs but is helpful for debugging)
|
||||||
const requestStr = JSON.stringify(requestBody);
|
const requestStr = JSON.stringify(requestBody);
|
||||||
log.info(`Full Ollama request (truncated): ${requestStr.substring(0, 1000)}...`);
|
log.info(`Full Ollama request (truncated): ${requestStr.substring(0, 1000)}...`);
|
||||||
log.info(`========== END OLLAMA REQUEST ==========`);
|
log.info(`========== END OLLAMA REQUEST ==========`);
|
||||||
|
|
||||||
// Make API request
|
// Make API request
|
||||||
const response = await fetch(`${apiBase}/api/chat`, {
|
const response = await fetch(`${apiBase}/api/chat`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -214,20 +214,20 @@ export class OllamaService extends BaseAIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data: OllamaResponse = await response.json();
|
const data: OllamaResponse = await response.json();
|
||||||
|
|
||||||
// Log response details
|
// Log response details
|
||||||
log.info(`========== OLLAMA API RESPONSE ==========`);
|
log.info(`========== OLLAMA API RESPONSE ==========`);
|
||||||
log.info(`Model: ${data.model}, Content length: ${data.message.content.length} chars`);
|
log.info(`Model: ${data.model}, Content length: ${data.message.content.length} chars`);
|
||||||
log.info(`Tokens: ${data.prompt_eval_count} prompt, ${data.eval_count} completion, ${data.prompt_eval_count + data.eval_count} total`);
|
log.info(`Tokens: ${data.prompt_eval_count} prompt, ${data.eval_count} completion, ${data.prompt_eval_count + data.eval_count} total`);
|
||||||
log.info(`Duration: ${data.total_duration}ns total, ${data.prompt_eval_duration}ns prompt, ${data.eval_duration}ns completion`);
|
log.info(`Duration: ${data.total_duration}ns total, ${data.prompt_eval_duration}ns prompt, ${data.eval_duration}ns completion`);
|
||||||
log.info(`Done: ${data.done}, Reason: ${data.done_reason || 'not specified'}`);
|
log.info(`Done: ${data.done}, Reason: ${data.done_reason || 'not specified'}`);
|
||||||
|
|
||||||
// Log content preview
|
// Log content preview
|
||||||
const contentPreview = data.message.content.length > 300
|
const contentPreview = data.message.content.length > 300
|
||||||
? `${data.message.content.substring(0, 300)}...`
|
? `${data.message.content.substring(0, 300)}...`
|
||||||
: data.message.content;
|
: data.message.content;
|
||||||
log.info(`Response content: ${contentPreview}`);
|
log.info(`Response content: ${contentPreview}`);
|
||||||
|
|
||||||
// Handle the response and extract tool calls if present
|
// Handle the response and extract tool calls if present
|
||||||
const chatResponse: ChatResponse = {
|
const chatResponse: ChatResponse = {
|
||||||
text: data.message.content,
|
text: data.message.content,
|
||||||
@ -242,45 +242,47 @@ export class OllamaService extends BaseAIService {
|
|||||||
|
|
||||||
// Add tool calls if present
|
// Add tool calls if present
|
||||||
if (data.message.tool_calls && data.message.tool_calls.length > 0) {
|
if (data.message.tool_calls && data.message.tool_calls.length > 0) {
|
||||||
|
log.info(`========== OLLAMA TOOL CALLS DETECTED ==========`);
|
||||||
log.info(`Ollama response includes ${data.message.tool_calls.length} tool calls`);
|
log.info(`Ollama response includes ${data.message.tool_calls.length} tool calls`);
|
||||||
|
|
||||||
// Log detailed information about each tool call
|
// Log detailed information about each tool call
|
||||||
const transformedToolCalls: ToolCall[] = [];
|
const transformedToolCalls: ToolCall[] = [];
|
||||||
|
|
||||||
// Log detailed information about the tool calls in the response
|
// Log detailed information about the tool calls in the response
|
||||||
log.info(`========== OLLAMA TOOL CALLS IN RESPONSE ==========`);
|
log.info(`========== OLLAMA TOOL CALLS IN RESPONSE ==========`);
|
||||||
data.message.tool_calls.forEach((toolCall, index) => {
|
data.message.tool_calls.forEach((toolCall, index) => {
|
||||||
log.info(`Tool call ${index + 1}:`);
|
log.info(`Tool call ${index + 1}:`);
|
||||||
log.info(` Name: ${toolCall.function?.name || 'unknown'}`);
|
log.info(` Name: ${toolCall.function?.name || 'unknown'}`);
|
||||||
log.info(` ID: ${toolCall.id || `auto-${index + 1}`}`);
|
log.info(` ID: ${toolCall.id || `auto-${index + 1}`}`);
|
||||||
|
|
||||||
// Generate a unique ID if none is provided
|
// Generate a unique ID if none is provided
|
||||||
const id = toolCall.id || `tool-call-${Date.now()}-${index}`;
|
const id = toolCall.id || `tool-call-${Date.now()}-${index}`;
|
||||||
|
|
||||||
// Handle arguments based on their type
|
// Handle arguments based on their type
|
||||||
let processedArguments: Record<string, any> | string;
|
let processedArguments: Record<string, any> | string;
|
||||||
|
|
||||||
if (typeof toolCall.function.arguments === 'string') {
|
if (typeof toolCall.function.arguments === 'string') {
|
||||||
// Log raw string arguments in full for debugging
|
// Log raw string arguments in full for debugging
|
||||||
log.info(` Raw string arguments: ${toolCall.function.arguments}`);
|
log.info(` Raw string arguments: ${toolCall.function.arguments}`);
|
||||||
|
|
||||||
// Try to parse JSON string arguments
|
// Try to parse JSON string arguments
|
||||||
try {
|
try {
|
||||||
processedArguments = JSON.parse(toolCall.function.arguments);
|
processedArguments = JSON.parse(toolCall.function.arguments);
|
||||||
log.info(` Successfully parsed arguments to object with keys: ${Object.keys(processedArguments).join(', ')}`);
|
log.info(` Successfully parsed arguments to object with keys: ${Object.keys(processedArguments).join(', ')}`);
|
||||||
log.info(` Parsed argument values:`);
|
log.info(` Parsed argument values:`);
|
||||||
Object.entries(processedArguments).forEach(([key, value]) => {
|
Object.entries(processedArguments).forEach(([key, value]) => {
|
||||||
const valuePreview = typeof value === 'string'
|
const valuePreview = typeof value === 'string'
|
||||||
? (value.length > 100 ? `${value.substring(0, 100)}...` : value)
|
? (value.length > 100 ? `${value.substring(0, 100)}...` : value)
|
||||||
: JSON.stringify(value);
|
: JSON.stringify(value);
|
||||||
log.info(` ${key}: ${valuePreview}`);
|
log.info(` ${key}: ${valuePreview}`);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e: unknown) {
|
||||||
// If parsing fails, keep as string and log the error
|
// If parsing fails, keep as string and log the error
|
||||||
processedArguments = toolCall.function.arguments;
|
processedArguments = toolCall.function.arguments;
|
||||||
log.info(` Could not parse arguments as JSON: ${e.message}`);
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||||
|
log.info(` Could not parse arguments as JSON: ${errorMessage}`);
|
||||||
log.info(` Keeping as string: ${processedArguments.substring(0, 200)}${processedArguments.length > 200 ? '...' : ''}`);
|
log.info(` Keeping as string: ${processedArguments.substring(0, 200)}${processedArguments.length > 200 ? '...' : ''}`);
|
||||||
|
|
||||||
// Try to clean and parse again with more aggressive methods
|
// Try to clean and parse again with more aggressive methods
|
||||||
try {
|
try {
|
||||||
const cleaned = toolCall.function.arguments
|
const cleaned = toolCall.function.arguments
|
||||||
@ -288,12 +290,13 @@ export class OllamaService extends BaseAIService {
|
|||||||
.replace(/\\"/g, '"') // Replace escaped quotes
|
.replace(/\\"/g, '"') // Replace escaped quotes
|
||||||
.replace(/([{,])\s*'([^']+)'\s*:/g, '$1"$2":') // Replace single quotes around property names
|
.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
|
.replace(/([{,])\s*(\w+)\s*:/g, '$1"$2":'); // Add quotes around unquoted property names
|
||||||
|
|
||||||
log.info(` Attempting to parse cleaned argument: ${cleaned}`);
|
log.info(` Attempting to parse cleaned argument: ${cleaned}`);
|
||||||
const reparseArg = JSON.parse(cleaned);
|
const reparseArg = JSON.parse(cleaned);
|
||||||
log.info(` Successfully parsed cleaned argument with keys: ${Object.keys(reparseArg).join(', ')}`);
|
log.info(` Successfully parsed cleaned argument with keys: ${Object.keys(reparseArg).join(', ')}`);
|
||||||
} catch (cleanErr) {
|
} catch (cleanErr: unknown) {
|
||||||
log.info(` Failed to parse cleaned arguments: ${cleanErr.message}`);
|
const cleanErrMessage = cleanErr instanceof Error ? cleanErr.message : String(cleanErr);
|
||||||
|
log.info(` Failed to parse cleaned arguments: ${cleanErrMessage}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -302,13 +305,13 @@ export class OllamaService extends BaseAIService {
|
|||||||
log.info(` Object arguments with keys: ${Object.keys(processedArguments).join(', ')}`);
|
log.info(` Object arguments with keys: ${Object.keys(processedArguments).join(', ')}`);
|
||||||
log.info(` Argument values:`);
|
log.info(` Argument values:`);
|
||||||
Object.entries(processedArguments).forEach(([key, value]) => {
|
Object.entries(processedArguments).forEach(([key, value]) => {
|
||||||
const valuePreview = typeof value === 'string'
|
const valuePreview = typeof value === 'string'
|
||||||
? (value.length > 100 ? `${value.substring(0, 100)}...` : value)
|
? (value.length > 100 ? `${value.substring(0, 100)}...` : value)
|
||||||
: JSON.stringify(value);
|
: JSON.stringify(value);
|
||||||
log.info(` ${key}: ${valuePreview}`);
|
log.info(` ${key}: ${valuePreview}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to our standard ToolCall format
|
// Convert to our standard ToolCall format
|
||||||
transformedToolCalls.push({
|
transformedToolCalls.push({
|
||||||
id,
|
id,
|
||||||
@ -319,11 +322,39 @@ export class OllamaService extends BaseAIService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add transformed tool calls to response
|
// Add transformed tool calls to response
|
||||||
chatResponse.tool_calls = transformedToolCalls;
|
chatResponse.tool_calls = transformedToolCalls;
|
||||||
log.info(`Transformed ${transformedToolCalls.length} tool calls for execution`);
|
log.info(`Transformed ${transformedToolCalls.length} tool calls for execution`);
|
||||||
|
log.info(`Tool calls after transformation: ${JSON.stringify(chatResponse.tool_calls)}`);
|
||||||
|
|
||||||
|
// CRITICAL: Explicitly mark response for tool execution
|
||||||
|
log.info(`CRITICAL: Explicitly marking response for tool execution`);
|
||||||
|
|
||||||
|
// Ensure tool_calls is properly exposed and formatted
|
||||||
|
// This is to make sure the pipeline can detect and execute the tools
|
||||||
|
if (transformedToolCalls.length > 0) {
|
||||||
|
// Make sure the tool_calls are exposed in the exact format expected by pipeline
|
||||||
|
chatResponse.tool_calls = transformedToolCalls.map(tc => ({
|
||||||
|
id: tc.id,
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: tc.function.name,
|
||||||
|
arguments: tc.function.arguments
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// If the content is empty, use a placeholder to avoid issues
|
||||||
|
if (!chatResponse.text) {
|
||||||
|
chatResponse.text = "Processing your request...";
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Final tool_calls format for pipeline: ${JSON.stringify(chatResponse.tool_calls)}`);
|
||||||
|
}
|
||||||
log.info(`========== END OLLAMA TOOL CALLS ==========`);
|
log.info(`========== END OLLAMA TOOL CALLS ==========`);
|
||||||
|
} else {
|
||||||
|
log.info(`========== NO OLLAMA TOOL CALLS DETECTED ==========`);
|
||||||
|
log.info(`Checking raw message response format: ${JSON.stringify(data.message)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`========== END OLLAMA RESPONSE ==========`);
|
log.info(`========== END OLLAMA RESPONSE ==========`);
|
||||||
|
@ -4,7 +4,7 @@ import type { Message, ChatCompletionOptions } from "./ai_interface.js";
|
|||||||
import contextService from "./context_service.js";
|
import contextService from "./context_service.js";
|
||||||
import { LLM_CONSTANTS } from './constants/provider_constants.js';
|
import { LLM_CONSTANTS } from './constants/provider_constants.js';
|
||||||
import { ERROR_PROMPTS } from './constants/llm_prompt_constants.js';
|
import { ERROR_PROMPTS } from './constants/llm_prompt_constants.js';
|
||||||
import * as aiServiceManagerModule from "./ai_service_manager.js";
|
import aiServiceManagerImport from "./ai_service_manager.js";
|
||||||
import becca from "../../becca/becca.js";
|
import becca from "../../becca/becca.js";
|
||||||
import vectorStore from "./embeddings/index.js";
|
import vectorStore from "./embeddings/index.js";
|
||||||
import providerManager from "./providers/providers.js";
|
import providerManager from "./providers/providers.js";
|
||||||
@ -94,7 +94,7 @@ class RestChatService {
|
|||||||
|
|
||||||
// Try to access the manager - will create instance only if needed
|
// Try to access the manager - will create instance only if needed
|
||||||
try {
|
try {
|
||||||
const aiManager = aiServiceManagerModule.default;
|
const aiManager = aiServiceManagerImport.getInstance();
|
||||||
|
|
||||||
if (!aiManager) {
|
if (!aiManager) {
|
||||||
log.info("AI check failed: AI manager module is not available");
|
log.info("AI check failed: AI manager module is not available");
|
||||||
@ -315,7 +315,7 @@ class RestChatService {
|
|||||||
log.info("AI services are not available - checking for specific issues");
|
log.info("AI services are not available - checking for specific issues");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const aiManager = aiServiceManagerModule.default;
|
const aiManager = aiServiceManagerImport.getInstance();
|
||||||
|
|
||||||
if (!aiManager) {
|
if (!aiManager) {
|
||||||
log.error("AI service manager is not initialized");
|
log.error("AI service manager is not initialized");
|
||||||
@ -341,7 +341,7 @@ class RestChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the AI service manager
|
// Get the AI service manager
|
||||||
const aiServiceManager = aiServiceManagerModule.default.getInstance();
|
const aiServiceManager = aiServiceManagerImport.getInstance();
|
||||||
|
|
||||||
// Get the default service - just use the first available one
|
// Get the default service - just use the first available one
|
||||||
const availableProviders = aiServiceManager.getAvailableProviders();
|
const availableProviders = aiServiceManager.getAvailableProviders();
|
||||||
@ -468,6 +468,9 @@ class RestChatService {
|
|||||||
// Use the Trilium-specific approach
|
// Use the Trilium-specific approach
|
||||||
const contextNoteId = session.noteContext || null;
|
const contextNoteId = session.noteContext || null;
|
||||||
|
|
||||||
|
// Ensure tools are initialized to prevent tool execution issues
|
||||||
|
await this.ensureToolsInitialized();
|
||||||
|
|
||||||
// Log that we're calling contextService with the parameters
|
// Log that we're calling contextService with the parameters
|
||||||
log.info(`Using enhanced context with: noteId=${contextNoteId}, showThinking=${showThinking}`);
|
log.info(`Using enhanced context with: noteId=${contextNoteId}, showThinking=${showThinking}`);
|
||||||
|
|
||||||
@ -506,23 +509,67 @@ class RestChatService {
|
|||||||
temperature: session.metadata.temperature || 0.7,
|
temperature: session.metadata.temperature || 0.7,
|
||||||
maxTokens: session.metadata.maxTokens,
|
maxTokens: session.metadata.maxTokens,
|
||||||
model: session.metadata.model,
|
model: session.metadata.model,
|
||||||
stream: isStreamingRequest ? true : undefined
|
stream: isStreamingRequest ? true : undefined,
|
||||||
|
enableTools: true // Explicitly enable tools
|
||||||
};
|
};
|
||||||
|
|
||||||
// Process based on whether this is a streaming request
|
// Add a note indicating we're explicitly enabling tools
|
||||||
|
log.info(`Advanced context flow: explicitly enabling tools in chat options`);
|
||||||
|
|
||||||
|
// Process streaming responses differently
|
||||||
if (isStreamingRequest) {
|
if (isStreamingRequest) {
|
||||||
|
// Handle streaming using the existing method
|
||||||
await this.handleStreamingResponse(res, aiMessages, chatOptions, service, session);
|
await this.handleStreamingResponse(res, aiMessages, chatOptions, service, session);
|
||||||
} else {
|
} else {
|
||||||
// Non-streaming approach for POST requests
|
// For non-streaming requests, generate a completion synchronously
|
||||||
const response = await service.generateChatCompletion(aiMessages, chatOptions);
|
const response = await service.generateChatCompletion(aiMessages, chatOptions);
|
||||||
const aiResponse = response.text; // Extract the text from the response
|
|
||||||
|
|
||||||
// Store the assistant's response in the session
|
// Check if the response contains tool calls
|
||||||
session.messages.push({
|
if (response.tool_calls && response.tool_calls.length > 0) {
|
||||||
role: 'assistant',
|
log.info(`Advanced context non-streaming: detected ${response.tool_calls.length} tool calls in response`);
|
||||||
content: aiResponse,
|
log.info(`Tool calls details: ${JSON.stringify(response.tool_calls)}`);
|
||||||
timestamp: new Date()
|
|
||||||
});
|
try {
|
||||||
|
// Execute the tools
|
||||||
|
const toolResults = await this.executeToolCalls(response);
|
||||||
|
log.info(`Successfully executed ${toolResults.length} tool calls in advanced context flow`);
|
||||||
|
|
||||||
|
// Build updated messages with tool results
|
||||||
|
const toolMessages = [...aiMessages, {
|
||||||
|
role: 'assistant',
|
||||||
|
content: response.text || '',
|
||||||
|
tool_calls: response.tool_calls
|
||||||
|
}, ...toolResults];
|
||||||
|
|
||||||
|
// Make a follow-up request with the tool results
|
||||||
|
log.info(`Making follow-up request with ${toolResults.length} tool results`);
|
||||||
|
const followUpOptions = {...chatOptions, enableTools: false}; // Disable tools for follow-up
|
||||||
|
const followUpResponse = await service.generateChatCompletion(toolMessages, followUpOptions);
|
||||||
|
|
||||||
|
// Update the session with the final response
|
||||||
|
session.messages.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: followUpResponse.text || '',
|
||||||
|
timestamp: new Date()
|
||||||
|
});
|
||||||
|
} catch (toolError: any) {
|
||||||
|
log.error(`Error executing tools in advanced context: ${toolError.message}`);
|
||||||
|
|
||||||
|
// Add error response to session
|
||||||
|
session.messages.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: `Error executing tools: ${toolError.message}`,
|
||||||
|
timestamp: new Date()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No tool calls, just add the response to the session
|
||||||
|
session.messages.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: response.text || '',
|
||||||
|
timestamp: new Date()
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sourceNotes;
|
return sourceNotes;
|
||||||
@ -608,50 +655,57 @@ class RestChatService {
|
|||||||
try {
|
try {
|
||||||
// Use the correct method name: generateChatCompletion
|
// Use the correct method name: generateChatCompletion
|
||||||
const response = await service.generateChatCompletion(aiMessages, chatOptions);
|
const response = await service.generateChatCompletion(aiMessages, chatOptions);
|
||||||
|
|
||||||
// Check for tool calls in the response
|
// Check for tool calls in the response
|
||||||
if (response.tool_calls && response.tool_calls.length > 0) {
|
if (response.tool_calls && response.tool_calls.length > 0) {
|
||||||
log.info(`========== STREAMING TOOL CALLS DETECTED ==========`);
|
log.info(`========== STREAMING TOOL CALLS DETECTED ==========`);
|
||||||
log.info(`Response contains ${response.tool_calls.length} tool calls, executing them...`);
|
log.info(`Response contains ${response.tool_calls.length} tool calls, executing them...`);
|
||||||
|
log.info(`CRITICAL CHECK: Tool execution is supposed to happen in the pipeline, not directly here.`);
|
||||||
|
log.info(`If tools are being executed here instead of in the pipeline, this may be a flow issue.`);
|
||||||
|
log.info(`Response came from provider: ${response.provider || 'unknown'}, model: ${response.model || 'unknown'}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
log.info(`========== STREAMING TOOL EXECUTION PATH ==========`);
|
||||||
|
log.info(`About to execute tools in streaming path (this is separate from pipeline tool execution)`);
|
||||||
|
|
||||||
// Execute the tools
|
// Execute the tools
|
||||||
const toolResults = await this.executeToolCalls(response);
|
const toolResults = await this.executeToolCalls(response);
|
||||||
|
log.info(`Successfully executed ${toolResults.length} tool calls in streaming path`);
|
||||||
|
|
||||||
// Make a follow-up request with the tool results
|
// Make a follow-up request with the tool results
|
||||||
const toolMessages = [...aiMessages, {
|
const toolMessages = [...aiMessages, {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: response.text || '',
|
content: response.text || '',
|
||||||
tool_calls: response.tool_calls
|
tool_calls: response.tool_calls
|
||||||
}, ...toolResults];
|
}, ...toolResults];
|
||||||
|
|
||||||
log.info(`Making follow-up request with ${toolResults.length} tool results`);
|
log.info(`Making follow-up request with ${toolResults.length} tool results`);
|
||||||
|
|
||||||
// Send partial response to let the client know tools are being processed
|
// Send partial response to let the client know tools are being processed
|
||||||
if (!res.writableEnded) {
|
if (!res.writableEnded) {
|
||||||
res.write(`data: ${JSON.stringify({ content: "Processing tools... " })}\n\n`);
|
res.write(`data: ${JSON.stringify({ content: "Processing tools... " })}\n\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use non-streaming for the follow-up to get a complete response
|
// Use non-streaming for the follow-up to get a complete response
|
||||||
const followUpOptions = {...chatOptions, stream: false, enableTools: false}; // Prevent infinite loops
|
const followUpOptions = {...chatOptions, stream: false, enableTools: false}; // Prevent infinite loops
|
||||||
const followUpResponse = await service.generateChatCompletion(toolMessages, followUpOptions);
|
const followUpResponse = await service.generateChatCompletion(toolMessages, followUpOptions);
|
||||||
|
|
||||||
messageContent = followUpResponse.text || "";
|
messageContent = followUpResponse.text || "";
|
||||||
|
|
||||||
// Send the complete response as a single chunk
|
// Send the complete response as a single chunk
|
||||||
if (!res.writableEnded) {
|
if (!res.writableEnded) {
|
||||||
res.write(`data: ${JSON.stringify({ content: messageContent })}\n\n`);
|
res.write(`data: ${JSON.stringify({ content: messageContent })}\n\n`);
|
||||||
res.write('data: [DONE]\n\n');
|
res.write('data: [DONE]\n\n');
|
||||||
res.end();
|
res.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the full response for the session
|
// Store the full response for the session
|
||||||
session.messages.push({
|
session.messages.push({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: messageContent,
|
content: messageContent,
|
||||||
timestamp: new Date()
|
timestamp: new Date()
|
||||||
});
|
});
|
||||||
|
|
||||||
return; // Skip the rest of the processing
|
return; // Skip the rest of the processing
|
||||||
} catch (toolError) {
|
} catch (toolError) {
|
||||||
log.error(`Error executing tools: ${toolError}`);
|
log.error(`Error executing tools: ${toolError}`);
|
||||||
@ -716,48 +770,55 @@ class RestChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute tool calls from the LLM response
|
* Execute tool calls from the LLM response
|
||||||
* @param response The LLM response containing tool calls
|
* @param response The LLM response containing tool calls
|
||||||
*/
|
*/
|
||||||
private async executeToolCalls(response: any): Promise<Message[]> {
|
private async executeToolCalls(response: any): Promise<Message[]> {
|
||||||
|
log.info(`========== REST SERVICE TOOL EXECUTION FLOW ==========`);
|
||||||
|
log.info(`Entered executeToolCalls method in REST chat service`);
|
||||||
|
|
||||||
if (!response.tool_calls || response.tool_calls.length === 0) {
|
if (!response.tool_calls || response.tool_calls.length === 0) {
|
||||||
|
log.info(`No tool calls to execute, returning early`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`Executing ${response.tool_calls.length} tool calls from REST chat service`);
|
log.info(`Executing ${response.tool_calls.length} tool calls from REST chat service`);
|
||||||
|
|
||||||
// Import tool registry directly to avoid circular dependencies
|
// Import tool registry directly to avoid circular dependencies
|
||||||
const toolRegistry = (await import('./tools/tool_registry.js')).default;
|
const toolRegistry = (await import('./tools/tool_registry.js')).default;
|
||||||
|
|
||||||
// Check if tools are available
|
// Check if tools are available
|
||||||
const availableTools = toolRegistry.getAllTools();
|
const availableTools = toolRegistry.getAllTools();
|
||||||
|
log.info(`Available tools in registry: ${availableTools.length}`);
|
||||||
|
|
||||||
if (availableTools.length === 0) {
|
if (availableTools.length === 0) {
|
||||||
log.error('No tools available in registry for execution');
|
log.error('No tools available in registry for execution');
|
||||||
|
|
||||||
// Try to initialize tools
|
// Try to initialize tools
|
||||||
try {
|
try {
|
||||||
const toolInitializer = await import('./tools/tool_initializer.js');
|
const toolInitializer = await import('./tools/tool_initializer.js');
|
||||||
await toolInitializer.default.initializeTools();
|
await toolInitializer.default.initializeTools();
|
||||||
log.info(`Initialized ${toolRegistry.getAllTools().length} tools`);
|
log.info(`Initialized ${toolRegistry.getAllTools().length} tools`);
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
log.error(`Failed to initialize tools: ${error}`);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
log.error(`Failed to initialize tools: ${errorMessage}`);
|
||||||
throw new Error('Tool execution failed: No tools available');
|
throw new Error('Tool execution failed: No tools available');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute each tool call and collect results
|
// Execute each tool call and collect results
|
||||||
const toolResults = await Promise.all(response.tool_calls.map(async (toolCall: any) => {
|
const toolResults = await Promise.all(response.tool_calls.map(async (toolCall: any) => {
|
||||||
try {
|
try {
|
||||||
log.info(`Executing tool: ${toolCall.function.name}, ID: ${toolCall.id || 'unknown'}`);
|
log.info(`Executing tool: ${toolCall.function.name}, ID: ${toolCall.id || 'unknown'}`);
|
||||||
|
|
||||||
// Get the tool from registry
|
// Get the tool from registry
|
||||||
const tool = toolRegistry.getTool(toolCall.function.name);
|
const tool = toolRegistry.getTool(toolCall.function.name);
|
||||||
if (!tool) {
|
if (!tool) {
|
||||||
throw new Error(`Tool not found: ${toolCall.function.name}`);
|
throw new Error(`Tool not found: ${toolCall.function.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse arguments
|
// Parse arguments
|
||||||
let args;
|
let args;
|
||||||
if (typeof toolCall.function.arguments === 'string') {
|
if (typeof toolCall.function.arguments === 'string') {
|
||||||
@ -765,7 +826,7 @@ class RestChatService {
|
|||||||
args = JSON.parse(toolCall.function.arguments);
|
args = JSON.parse(toolCall.function.arguments);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error(`Failed to parse tool arguments: ${e.message}`);
|
log.error(`Failed to parse tool arguments: ${e.message}`);
|
||||||
|
|
||||||
// Try cleanup and retry
|
// Try cleanup and retry
|
||||||
try {
|
try {
|
||||||
const cleaned = toolCall.function.arguments
|
const cleaned = toolCall.function.arguments
|
||||||
@ -773,7 +834,7 @@ class RestChatService {
|
|||||||
.replace(/\\"/g, '"') // Replace escaped quotes
|
.replace(/\\"/g, '"') // Replace escaped quotes
|
||||||
.replace(/([{,])\s*'([^']+)'\s*:/g, '$1"$2":') // Replace single quotes around property names
|
.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
|
.replace(/([{,])\s*(\w+)\s*:/g, '$1"$2":'); // Add quotes around unquoted property names
|
||||||
|
|
||||||
args = JSON.parse(cleaned);
|
args = JSON.parse(cleaned);
|
||||||
} catch (cleanErr) {
|
} catch (cleanErr) {
|
||||||
// If all parsing fails, use as-is
|
// If all parsing fails, use as-is
|
||||||
@ -783,23 +844,23 @@ class RestChatService {
|
|||||||
} else {
|
} else {
|
||||||
args = toolCall.function.arguments;
|
args = toolCall.function.arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log what we're about to execute
|
// Log what we're about to execute
|
||||||
log.info(`Executing tool with arguments: ${JSON.stringify(args)}`);
|
log.info(`Executing tool with arguments: ${JSON.stringify(args)}`);
|
||||||
|
|
||||||
// Execute the tool and get result
|
// Execute the tool and get result
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const result = await tool.execute(args);
|
const result = await tool.execute(args);
|
||||||
const executionTime = Date.now() - startTime;
|
const executionTime = Date.now() - startTime;
|
||||||
|
|
||||||
log.info(`Tool execution completed in ${executionTime}ms`);
|
log.info(`Tool execution completed in ${executionTime}ms`);
|
||||||
|
|
||||||
// Log the result
|
// Log the result
|
||||||
const resultPreview = typeof result === 'string'
|
const resultPreview = typeof result === 'string'
|
||||||
? result.substring(0, 100) + (result.length > 100 ? '...' : '')
|
? result.substring(0, 100) + (result.length > 100 ? '...' : '')
|
||||||
: JSON.stringify(result).substring(0, 100) + '...';
|
: JSON.stringify(result).substring(0, 100) + '...';
|
||||||
log.info(`Tool result: ${resultPreview}`);
|
log.info(`Tool result: ${resultPreview}`);
|
||||||
|
|
||||||
// Format result as a proper message
|
// Format result as a proper message
|
||||||
return {
|
return {
|
||||||
role: 'tool',
|
role: 'tool',
|
||||||
@ -809,7 +870,7 @@ class RestChatService {
|
|||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
log.error(`Error executing tool ${toolCall.function.name}: ${error.message}`);
|
log.error(`Error executing tool ${toolCall.function.name}: ${error.message}`);
|
||||||
|
|
||||||
// Return error as tool result
|
// Return error as tool result
|
||||||
return {
|
return {
|
||||||
role: 'tool',
|
role: 'tool',
|
||||||
@ -819,7 +880,7 @@ class RestChatService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
log.info(`Completed execution of ${toolResults.length} tools`);
|
log.info(`Completed execution of ${toolResults.length} tools`);
|
||||||
return toolResults;
|
return toolResults;
|
||||||
}
|
}
|
||||||
@ -1042,6 +1103,33 @@ class RestChatService {
|
|||||||
throw new Error(`Failed to delete session: ${error.message || 'Unknown error'}`);
|
throw new Error(`Failed to delete session: ${error.message || 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that LLM tools are properly initialized
|
||||||
|
* This helps prevent issues with tool execution
|
||||||
|
*/
|
||||||
|
private async ensureToolsInitialized(): Promise<void> {
|
||||||
|
try {
|
||||||
|
log.info("Initializing LLM agent tools...");
|
||||||
|
|
||||||
|
// Initialize LLM tools without depending on aiServiceManager
|
||||||
|
const toolInitializer = await import('./tools/tool_initializer.js');
|
||||||
|
await toolInitializer.default.initializeTools();
|
||||||
|
|
||||||
|
// Get the tool registry to check if tools were initialized
|
||||||
|
const toolRegistry = (await import('./tools/tool_registry.js')).default;
|
||||||
|
const tools = toolRegistry.getAllTools();
|
||||||
|
log.info(`LLM tools initialized successfully: ${tools.length} tools available`);
|
||||||
|
|
||||||
|
// Log available tools
|
||||||
|
if (tools.length > 0) {
|
||||||
|
log.info(`Available tools: ${tools.map(t => t.definition.function.name).join(', ')}`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error initializing LLM tools: ${error.message}`);
|
||||||
|
// Don't throw, just log the error to prevent breaking the pipeline
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create singleton instance
|
// Create singleton instance
|
||||||
|
226
src/services/llm/tools/attribute_manager_tool.ts
Normal file
226
src/services/llm/tools/attribute_manager_tool.ts
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
/**
|
||||||
|
* Attribute Manager Tool
|
||||||
|
*
|
||||||
|
* This tool allows the LLM to add, remove, or modify note attributes in Trilium.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Tool, ToolHandler } from './tool_interfaces.js';
|
||||||
|
import log from '../../log.js';
|
||||||
|
import becca from '../../../becca/becca.js';
|
||||||
|
import attributes from '../../attributes.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition of the attribute manager tool
|
||||||
|
*/
|
||||||
|
export const attributeManagerToolDefinition: Tool = {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'manage_attributes',
|
||||||
|
description: 'Add, remove, or modify attributes (labels/relations) on a note',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
noteId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID of the note to manage attributes for'
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Action to perform on the attribute',
|
||||||
|
enum: ['add', 'remove', 'update', 'list']
|
||||||
|
},
|
||||||
|
attributeName: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Name of the attribute (e.g., "#tag" for a label, or "relation" for a relation)'
|
||||||
|
},
|
||||||
|
attributeValue: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Value of the attribute (for add/update actions). Not needed for label-type attributes.'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['noteId', 'action']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute manager tool implementation
|
||||||
|
*/
|
||||||
|
export class AttributeManagerTool implements ToolHandler {
|
||||||
|
public definition: Tool = attributeManagerToolDefinition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the attribute manager tool
|
||||||
|
*/
|
||||||
|
public async execute(args: { noteId: string, action: string, attributeName?: string, attributeValue?: string }): Promise<string | object> {
|
||||||
|
try {
|
||||||
|
const { noteId, action, attributeName, attributeValue } = args;
|
||||||
|
|
||||||
|
log.info(`Executing manage_attributes tool - NoteID: "${noteId}", Action: ${action}, AttributeName: ${attributeName || 'not specified'}`);
|
||||||
|
|
||||||
|
// 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})`);
|
||||||
|
|
||||||
|
// List all existing attributes
|
||||||
|
if (action === 'list') {
|
||||||
|
const noteAttributes = note.getOwnedAttributes();
|
||||||
|
log.info(`Listing ${noteAttributes.length} attributes for note "${note.title}"`);
|
||||||
|
|
||||||
|
const formattedAttributes = noteAttributes.map(attr => ({
|
||||||
|
name: attr.name,
|
||||||
|
value: attr.value,
|
||||||
|
type: attr.type
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
noteId: note.noteId,
|
||||||
|
title: note.title,
|
||||||
|
attributeCount: noteAttributes.length,
|
||||||
|
attributes: formattedAttributes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other actions, attribute name is required
|
||||||
|
if (!attributeName) {
|
||||||
|
return 'Error: attributeName is required for add, remove, and update actions';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the requested action
|
||||||
|
if (action === 'add') {
|
||||||
|
// Add a new attribute
|
||||||
|
try {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// For label-type attributes (starting with #), no value is needed
|
||||||
|
const isLabel = attributeName.startsWith('#');
|
||||||
|
const value = isLabel ? '' : (attributeValue || '');
|
||||||
|
|
||||||
|
// Check if attribute already exists
|
||||||
|
const existingAttrs = note.getOwnedAttributes()
|
||||||
|
.filter(attr => attr.name === attributeName && attr.value === value);
|
||||||
|
|
||||||
|
if (existingAttrs.length > 0) {
|
||||||
|
log.info(`Attribute ${attributeName}=${value} already exists on note "${note.title}"`);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Attribute ${attributeName}=${value || ''} already exists on note "${note.title}"`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the attribute
|
||||||
|
await attributes.createAttribute(noteId, attributeName, value);
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
log.info(`Added attribute ${attributeName}=${value || ''} in ${duration}ms`);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
noteId: note.noteId,
|
||||||
|
title: note.title,
|
||||||
|
action: 'add',
|
||||||
|
attributeName: attributeName,
|
||||||
|
attributeValue: value,
|
||||||
|
message: `Added attribute ${attributeName}=${value || ''} to note "${note.title}"`
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error adding attribute: ${error.message || String(error)}`);
|
||||||
|
return `Error: ${error.message || String(error)}`;
|
||||||
|
}
|
||||||
|
} else if (action === 'remove') {
|
||||||
|
// Remove an attribute
|
||||||
|
try {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// Find the attribute to remove
|
||||||
|
const attributesToRemove = note.getOwnedAttributes()
|
||||||
|
.filter(attr => attr.name === attributeName &&
|
||||||
|
(attributeValue === undefined || attr.value === attributeValue));
|
||||||
|
|
||||||
|
if (attributesToRemove.length === 0) {
|
||||||
|
log.info(`Attribute ${attributeName} not found on note "${note.title}"`);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Attribute ${attributeName} not found on note "${note.title}"`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all matching attributes
|
||||||
|
for (const attr of attributesToRemove) {
|
||||||
|
await attributes.deleteAttribute(attr.attributeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
log.info(`Removed ${attributesToRemove.length} attribute(s) in ${duration}ms`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
noteId: note.noteId,
|
||||||
|
title: note.title,
|
||||||
|
action: 'remove',
|
||||||
|
attributeName: attributeName,
|
||||||
|
attributesRemoved: attributesToRemove.length,
|
||||||
|
message: `Removed ${attributesToRemove.length} attribute(s) from note "${note.title}"`
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error removing attribute: ${error.message || String(error)}`);
|
||||||
|
return `Error: ${error.message || String(error)}`;
|
||||||
|
}
|
||||||
|
} else if (action === 'update') {
|
||||||
|
// Update an attribute
|
||||||
|
try {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
if (attributeValue === undefined) {
|
||||||
|
return 'Error: attributeValue is required for update action';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the attribute to update
|
||||||
|
const attributesToUpdate = note.getOwnedAttributes()
|
||||||
|
.filter(attr => attr.name === attributeName);
|
||||||
|
|
||||||
|
if (attributesToUpdate.length === 0) {
|
||||||
|
log.info(`Attribute ${attributeName} not found on note "${note.title}"`);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Attribute ${attributeName} not found on note "${note.title}"`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all matching attributes
|
||||||
|
for (const attr of attributesToUpdate) {
|
||||||
|
await attributes.updateAttributeValue(attr.attributeId, attributeValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
log.info(`Updated ${attributesToUpdate.length} attribute(s) in ${duration}ms`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
noteId: note.noteId,
|
||||||
|
title: note.title,
|
||||||
|
action: 'update',
|
||||||
|
attributeName: attributeName,
|
||||||
|
attributeValue: attributeValue,
|
||||||
|
attributesUpdated: attributesToUpdate.length,
|
||||||
|
message: `Updated ${attributesToUpdate.length} attribute(s) on note "${note.title}"`
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error updating attribute: ${error.message || String(error)}`);
|
||||||
|
return `Error: ${error.message || String(error)}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return `Error: Unsupported action "${action}". Supported actions are: add, remove, update, list`;
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error executing manage_attributes tool: ${error.message || String(error)}`);
|
||||||
|
return `Error: ${error.message || String(error)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
481
src/services/llm/tools/calendar_integration_tool.ts
Normal file
481
src/services/llm/tools/calendar_integration_tool.ts
Normal file
@ -0,0 +1,481 @@
|
|||||||
|
/**
|
||||||
|
* Calendar Integration Tool
|
||||||
|
*
|
||||||
|
* This tool allows the LLM to find date-related notes or create date-based entries.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Tool, ToolHandler } from './tool_interfaces.js';
|
||||||
|
import log from '../../log.js';
|
||||||
|
import becca from '../../../becca/becca.js';
|
||||||
|
import notes from '../../notes.js';
|
||||||
|
import attributes from '../../attributes.js';
|
||||||
|
import dateNotes from '../../date_notes.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition of the calendar integration tool
|
||||||
|
*/
|
||||||
|
export const calendarIntegrationToolDefinition: Tool = {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'calendar_integration',
|
||||||
|
description: 'Find date-related notes or create date-based entries',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
action: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Action to perform',
|
||||||
|
enum: ['find_date_notes', 'create_date_note', 'find_notes_with_date_range', 'get_daily_note']
|
||||||
|
},
|
||||||
|
date: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Date in ISO format (YYYY-MM-DD) for the note'
|
||||||
|
},
|
||||||
|
dateStart: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Start date in ISO format (YYYY-MM-DD) for date range queries'
|
||||||
|
},
|
||||||
|
dateEnd: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'End date in ISO format (YYYY-MM-DD) for date range queries'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Title for creating a new date-related note'
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Content for creating a new date-related note'
|
||||||
|
},
|
||||||
|
parentNoteId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Optional parent note ID for the new date note. If not specified, will use default calendar container.'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['action']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calendar integration tool implementation
|
||||||
|
*/
|
||||||
|
export class CalendarIntegrationTool implements ToolHandler {
|
||||||
|
public definition: Tool = calendarIntegrationToolDefinition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the calendar integration tool
|
||||||
|
*/
|
||||||
|
public async execute(args: {
|
||||||
|
action: string,
|
||||||
|
date?: string,
|
||||||
|
dateStart?: string,
|
||||||
|
dateEnd?: string,
|
||||||
|
title?: string,
|
||||||
|
content?: string,
|
||||||
|
parentNoteId?: string
|
||||||
|
}): Promise<string | object> {
|
||||||
|
try {
|
||||||
|
const { action, date, dateStart, dateEnd, title, content, parentNoteId } = args;
|
||||||
|
|
||||||
|
log.info(`Executing calendar_integration tool - Action: ${action}, Date: ${date || 'not specified'}`);
|
||||||
|
|
||||||
|
// Handle different actions
|
||||||
|
if (action === 'find_date_notes') {
|
||||||
|
return await this.findDateNotes(date);
|
||||||
|
} else if (action === 'create_date_note') {
|
||||||
|
return await this.createDateNote(date, title, content, parentNoteId);
|
||||||
|
} else if (action === 'find_notes_with_date_range') {
|
||||||
|
return await this.findNotesWithDateRange(dateStart, dateEnd);
|
||||||
|
} else if (action === 'get_daily_note') {
|
||||||
|
return await this.getDailyNote(date);
|
||||||
|
} else {
|
||||||
|
return `Error: Unsupported action "${action}". Supported actions are: find_date_notes, create_date_note, find_notes_with_date_range, get_daily_note`;
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error executing calendar_integration tool: ${error.message || String(error)}`);
|
||||||
|
return `Error: ${error.message || String(error)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find notes related to a specific date
|
||||||
|
*/
|
||||||
|
private async findDateNotes(date?: string): Promise<object> {
|
||||||
|
if (!date) {
|
||||||
|
// If no date is provided, use today's date
|
||||||
|
const today = new Date();
|
||||||
|
date = today.toISOString().split('T')[0];
|
||||||
|
log.info(`No date specified, using today's date: ${date}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Validate date format
|
||||||
|
if (!this.isValidDate(date)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Invalid date format. Please use YYYY-MM-DD format.`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Finding notes related to date: ${date}`);
|
||||||
|
|
||||||
|
// Get notes with dateNote attribute matching this date
|
||||||
|
const notesWithDateAttribute = this.getNotesWithDateAttribute(date);
|
||||||
|
log.info(`Found ${notesWithDateAttribute.length} notes with date attribute for ${date}`);
|
||||||
|
|
||||||
|
// Get year, month, day notes if they exist
|
||||||
|
const yearMonthDayNotes = await this.getYearMonthDayNotes(date);
|
||||||
|
|
||||||
|
// Format results
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
date: date,
|
||||||
|
yearNote: yearMonthDayNotes.yearNote ? {
|
||||||
|
noteId: yearMonthDayNotes.yearNote.noteId,
|
||||||
|
title: yearMonthDayNotes.yearNote.title
|
||||||
|
} : null,
|
||||||
|
monthNote: yearMonthDayNotes.monthNote ? {
|
||||||
|
noteId: yearMonthDayNotes.monthNote.noteId,
|
||||||
|
title: yearMonthDayNotes.monthNote.title
|
||||||
|
} : null,
|
||||||
|
dayNote: yearMonthDayNotes.dayNote ? {
|
||||||
|
noteId: yearMonthDayNotes.dayNote.noteId,
|
||||||
|
title: yearMonthDayNotes.dayNote.title
|
||||||
|
} : null,
|
||||||
|
relatedNotes: notesWithDateAttribute.map(note => ({
|
||||||
|
noteId: note.noteId,
|
||||||
|
title: note.title,
|
||||||
|
type: note.type
|
||||||
|
})),
|
||||||
|
message: `Found ${notesWithDateAttribute.length} notes related to date ${date}`
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error finding date notes: ${error.message || String(error)}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new note associated with a date
|
||||||
|
*/
|
||||||
|
private async createDateNote(date?: string, title?: string, content?: string, parentNoteId?: string): Promise<object> {
|
||||||
|
if (!date) {
|
||||||
|
// If no date is provided, use today's date
|
||||||
|
const today = new Date();
|
||||||
|
date = today.toISOString().split('T')[0];
|
||||||
|
log.info(`No date specified, using today's date: ${date}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate date format
|
||||||
|
if (!this.isValidDate(date)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Invalid date format. Please use YYYY-MM-DD format.`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
title = `Note for ${date}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
content = `<p>Date note created for ${date}</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info(`Creating new date note for ${date} with title "${title}"`);
|
||||||
|
|
||||||
|
// If no parent is specified, try to find appropriate date container
|
||||||
|
if (!parentNoteId) {
|
||||||
|
// Get or create day note to use as parent
|
||||||
|
const dateComponents = this.parseDateString(date);
|
||||||
|
if (!dateComponents) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Invalid date format. Please use YYYY-MM-DD format.`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the date string directly with getDayNote
|
||||||
|
const dayNote = await dateNotes.getDayNote(date);
|
||||||
|
|
||||||
|
if (dayNote) {
|
||||||
|
parentNoteId = dayNote.noteId;
|
||||||
|
log.info(`Using day note ${dayNote.title} (${parentNoteId}) as parent`);
|
||||||
|
} else {
|
||||||
|
// Use root if day note couldn't be found/created
|
||||||
|
parentNoteId = 'root';
|
||||||
|
log.info(`Could not find/create day note, using root as parent`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate parent note exists
|
||||||
|
const parent = becca.notes[parentNoteId];
|
||||||
|
if (!parent) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Parent note with ID ${parentNoteId} not found. Please specify a valid parent note ID.`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the new note
|
||||||
|
const createStartTime = Date.now();
|
||||||
|
const noteId = await notes.createNewNote({
|
||||||
|
parentNoteId: parent.noteId,
|
||||||
|
title: title,
|
||||||
|
content: content,
|
||||||
|
type: 'text',
|
||||||
|
mime: 'text/html'
|
||||||
|
});
|
||||||
|
const createDuration = Date.now() - createStartTime;
|
||||||
|
|
||||||
|
if (!noteId) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Failed to create date note. An unknown error occurred.`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Created new note with ID ${noteId} in ${createDuration}ms`);
|
||||||
|
|
||||||
|
// Add dateNote attribute with the specified date
|
||||||
|
const attrStartTime = Date.now();
|
||||||
|
await attributes.createLabel(noteId, 'dateNote', date);
|
||||||
|
const attrDuration = Date.now() - attrStartTime;
|
||||||
|
|
||||||
|
log.info(`Added dateNote=${date} attribute in ${attrDuration}ms`);
|
||||||
|
|
||||||
|
// Return the new note information
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
noteId: noteId,
|
||||||
|
date: date,
|
||||||
|
title: title,
|
||||||
|
message: `Created new date note "${title}" for ${date}`
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error creating date note: ${error.message || String(error)}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find notes with date attributes in a specified range
|
||||||
|
*/
|
||||||
|
private async findNotesWithDateRange(dateStart?: string, dateEnd?: string): Promise<object> {
|
||||||
|
if (!dateStart || !dateEnd) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Both dateStart and dateEnd are required for find_notes_with_date_range action.`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate date formats
|
||||||
|
if (!this.isValidDate(dateStart) || !this.isValidDate(dateEnd)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Invalid date format. Please use YYYY-MM-DD format.`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info(`Finding notes with date attributes in range ${dateStart} to ${dateEnd}`);
|
||||||
|
|
||||||
|
// Get all notes with dateNote attribute
|
||||||
|
const allNotes = this.getAllNotesWithDateAttribute();
|
||||||
|
|
||||||
|
// Filter by date range
|
||||||
|
const startDate = new Date(dateStart);
|
||||||
|
const endDate = new Date(dateEnd);
|
||||||
|
|
||||||
|
const filteredNotes = allNotes.filter(note => {
|
||||||
|
const dateAttr = note.getOwnedAttributes()
|
||||||
|
.find((attr: any) => attr.name === 'dateNote');
|
||||||
|
|
||||||
|
if (dateAttr && dateAttr.value) {
|
||||||
|
const noteDate = new Date(dateAttr.value);
|
||||||
|
return noteDate >= startDate && noteDate <= endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info(`Found ${filteredNotes.length} notes in date range`);
|
||||||
|
|
||||||
|
// Sort notes by date
|
||||||
|
filteredNotes.sort((a, b) => {
|
||||||
|
const aDateAttr = a.getOwnedAttributes().find((attr: any) => attr.name === 'dateNote');
|
||||||
|
const bDateAttr = b.getOwnedAttributes().find((attr: any) => attr.name === 'dateNote');
|
||||||
|
|
||||||
|
if (aDateAttr && bDateAttr) {
|
||||||
|
const aDate = new Date(aDateAttr.value);
|
||||||
|
const bDate = new Date(bDateAttr.value);
|
||||||
|
return aDate.getTime() - bDate.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Format results
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
dateStart: dateStart,
|
||||||
|
dateEnd: dateEnd,
|
||||||
|
noteCount: filteredNotes.length,
|
||||||
|
notes: filteredNotes.map(note => {
|
||||||
|
const dateAttr = note.getOwnedAttributes().find((attr: any) => attr.name === 'dateNote');
|
||||||
|
return {
|
||||||
|
noteId: note.noteId,
|
||||||
|
title: note.title,
|
||||||
|
type: note.type,
|
||||||
|
date: dateAttr ? dateAttr.value : null
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
message: `Found ${filteredNotes.length} notes in date range ${dateStart} to ${dateEnd}`
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error finding notes in date range: ${error.message || String(error)}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create a daily note for a specific date
|
||||||
|
*/
|
||||||
|
private async getDailyNote(date?: string): Promise<object> {
|
||||||
|
if (!date) {
|
||||||
|
// If no date is provided, use today's date
|
||||||
|
const today = new Date();
|
||||||
|
date = today.toISOString().split('T')[0];
|
||||||
|
log.info(`No date specified, using today's date: ${date}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate date format
|
||||||
|
if (!this.isValidDate(date)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Invalid date format. Please use YYYY-MM-DD format.`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info(`Getting daily note for ${date}`);
|
||||||
|
|
||||||
|
// Get or create day note - directly pass the date string
|
||||||
|
const startTime = Date.now();
|
||||||
|
const dayNote = await dateNotes.getDayNote(date);
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
if (!dayNote) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Could not find or create daily note for ${date}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Retrieved/created daily note for ${date} in ${duration}ms`);
|
||||||
|
|
||||||
|
// Get parent month and year notes
|
||||||
|
const yearStr = date.substring(0, 4);
|
||||||
|
const monthStr = date.substring(0, 7);
|
||||||
|
|
||||||
|
const monthNote = await dateNotes.getMonthNote(monthStr);
|
||||||
|
const yearNote = await dateNotes.getYearNote(yearStr);
|
||||||
|
|
||||||
|
// Return the note information
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
date: date,
|
||||||
|
dayNote: {
|
||||||
|
noteId: dayNote.noteId,
|
||||||
|
title: dayNote.title,
|
||||||
|
content: await dayNote.getContent()
|
||||||
|
},
|
||||||
|
monthNote: monthNote ? {
|
||||||
|
noteId: monthNote.noteId,
|
||||||
|
title: monthNote.title
|
||||||
|
} : null,
|
||||||
|
yearNote: yearNote ? {
|
||||||
|
noteId: yearNote.noteId,
|
||||||
|
title: yearNote.title
|
||||||
|
} : null,
|
||||||
|
message: `Retrieved daily note for ${date}`
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error getting daily note: ${error.message || String(error)}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to get notes with a specific date attribute
|
||||||
|
*/
|
||||||
|
private getNotesWithDateAttribute(date: string): any[] {
|
||||||
|
// Find notes with matching dateNote attribute
|
||||||
|
return attributes.getNotesWithLabel('dateNote', date) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to get all notes with any date attribute
|
||||||
|
*/
|
||||||
|
private getAllNotesWithDateAttribute(): any[] {
|
||||||
|
// Find all notes with dateNote attribute
|
||||||
|
return attributes.getNotesWithLabel('dateNote') || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to get year, month, and day notes for a date
|
||||||
|
*/
|
||||||
|
private async getYearMonthDayNotes(date: string): Promise<{
|
||||||
|
yearNote: any | null;
|
||||||
|
monthNote: any | null;
|
||||||
|
dayNote: any | null;
|
||||||
|
}> {
|
||||||
|
if (!this.isValidDate(date)) {
|
||||||
|
return { yearNote: null, monthNote: null, dayNote: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the year and month from the date string
|
||||||
|
const yearStr = date.substring(0, 4);
|
||||||
|
const monthStr = date.substring(0, 7);
|
||||||
|
|
||||||
|
// Use the dateNotes service to get the notes
|
||||||
|
const yearNote = await dateNotes.getYearNote(yearStr);
|
||||||
|
const monthNote = await dateNotes.getMonthNote(monthStr);
|
||||||
|
const dayNote = await dateNotes.getDayNote(date);
|
||||||
|
|
||||||
|
return { yearNote, monthNote, dayNote };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to validate date string format
|
||||||
|
*/
|
||||||
|
private isValidDate(dateString: string): boolean {
|
||||||
|
const regex = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
|
|
||||||
|
if (!regex.test(dateString)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toString() !== 'Invalid Date';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to parse date string into components
|
||||||
|
*/
|
||||||
|
private parseDateString(dateString: string): { year: number; month: number; day: number } | null {
|
||||||
|
if (!this.isValidDate(dateString)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [yearStr, monthStr, dayStr] = dateString.split('-');
|
||||||
|
|
||||||
|
return {
|
||||||
|
year: parseInt(yearStr, 10),
|
||||||
|
month: parseInt(monthStr, 10),
|
||||||
|
day: parseInt(dayStr, 10)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
544
src/services/llm/tools/content_extraction_tool.ts
Normal file
544
src/services/llm/tools/content_extraction_tool.ts
Normal file
@ -0,0 +1,544 @@
|
|||||||
|
/**
|
||||||
|
* Content Extraction Tool
|
||||||
|
*
|
||||||
|
* This tool allows the LLM to extract structured information from notes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Tool, ToolHandler } from './tool_interfaces.js';
|
||||||
|
import log from '../../log.js';
|
||||||
|
import becca from '../../../becca/becca.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition of the content extraction tool
|
||||||
|
*/
|
||||||
|
export const contentExtractionToolDefinition: Tool = {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'extract_content',
|
||||||
|
description: 'Extract structured information from a note\'s content, such as lists, tables, or specific sections',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
noteId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID of the note to extract content from'
|
||||||
|
},
|
||||||
|
extractionType: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Type of content to extract',
|
||||||
|
enum: ['lists', 'tables', 'headings', 'codeBlocks', 'all']
|
||||||
|
},
|
||||||
|
format: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Format to return the extracted content in',
|
||||||
|
enum: ['json', 'markdown', 'text']
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Optional search query to filter extracted content (e.g., "tasks related to finance")'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['noteId', 'extractionType']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content extraction tool implementation
|
||||||
|
*/
|
||||||
|
export class ContentExtractionTool implements ToolHandler {
|
||||||
|
public definition: Tool = contentExtractionToolDefinition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the content extraction tool
|
||||||
|
*/
|
||||||
|
public async execute(args: {
|
||||||
|
noteId: string,
|
||||||
|
extractionType: 'lists' | 'tables' | 'headings' | 'codeBlocks' | 'all',
|
||||||
|
format?: 'json' | 'markdown' | 'text',
|
||||||
|
query?: string
|
||||||
|
}): Promise<string | object> {
|
||||||
|
try {
|
||||||
|
const { noteId, extractionType, format = 'json', query } = args;
|
||||||
|
|
||||||
|
log.info(`Executing extract_content tool - NoteID: "${noteId}", Type: ${extractionType}, Format: ${format}`);
|
||||||
|
|
||||||
|
// 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 the note content
|
||||||
|
const content = await note.getContent();
|
||||||
|
if (!content) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Note content is empty'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Retrieved note content, length: ${content.length} chars`);
|
||||||
|
|
||||||
|
// Extract the requested content
|
||||||
|
const extractedContent: any = {};
|
||||||
|
|
||||||
|
if (extractionType === 'lists' || extractionType === 'all') {
|
||||||
|
extractedContent.lists = this.extractLists(content);
|
||||||
|
log.info(`Extracted ${extractedContent.lists.length} lists`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extractionType === 'tables' || extractionType === 'all') {
|
||||||
|
extractedContent.tables = this.extractTables(content);
|
||||||
|
log.info(`Extracted ${extractedContent.tables.length} tables`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extractionType === 'headings' || extractionType === 'all') {
|
||||||
|
extractedContent.headings = this.extractHeadings(content);
|
||||||
|
log.info(`Extracted ${extractedContent.headings.length} headings`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extractionType === 'codeBlocks' || extractionType === 'all') {
|
||||||
|
extractedContent.codeBlocks = this.extractCodeBlocks(content);
|
||||||
|
log.info(`Extracted ${extractedContent.codeBlocks.length} code blocks`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by query if provided
|
||||||
|
if (query) {
|
||||||
|
log.info(`Filtering extracted content with query: "${query}"`);
|
||||||
|
this.filterContentByQuery(extractedContent, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the response based on requested format
|
||||||
|
if (format === 'markdown') {
|
||||||
|
return this.formatAsMarkdown(extractedContent, extractionType);
|
||||||
|
} else if (format === 'text') {
|
||||||
|
return this.formatAsText(extractedContent, extractionType);
|
||||||
|
} else {
|
||||||
|
// Default to JSON format
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
noteId: note.noteId,
|
||||||
|
title: note.title,
|
||||||
|
extractionType,
|
||||||
|
content: extractedContent
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error executing extract_content tool: ${error.message || String(error)}`);
|
||||||
|
return `Error: ${error.message || String(error)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract lists from HTML content
|
||||||
|
*/
|
||||||
|
private extractLists(content: string): Array<{ type: string, items: string[] }> {
|
||||||
|
const lists = [];
|
||||||
|
|
||||||
|
// Extract unordered lists
|
||||||
|
const ulRegex = /<ul[^>]*>([\s\S]*?)<\/ul>/gi;
|
||||||
|
let ulMatch;
|
||||||
|
|
||||||
|
while ((ulMatch = ulRegex.exec(content)) !== null) {
|
||||||
|
const listContent = ulMatch[1];
|
||||||
|
const items = this.extractListItems(listContent);
|
||||||
|
|
||||||
|
if (items.length > 0) {
|
||||||
|
lists.push({
|
||||||
|
type: 'unordered',
|
||||||
|
items
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract ordered lists
|
||||||
|
const olRegex = /<ol[^>]*>([\s\S]*?)<\/ol>/gi;
|
||||||
|
let olMatch;
|
||||||
|
|
||||||
|
while ((olMatch = olRegex.exec(content)) !== null) {
|
||||||
|
const listContent = olMatch[1];
|
||||||
|
const items = this.extractListItems(listContent);
|
||||||
|
|
||||||
|
if (items.length > 0) {
|
||||||
|
lists.push({
|
||||||
|
type: 'ordered',
|
||||||
|
items
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lists;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract list items from list content
|
||||||
|
*/
|
||||||
|
private extractListItems(listContent: string): string[] {
|
||||||
|
const items = [];
|
||||||
|
const itemRegex = /<li[^>]*>([\s\S]*?)<\/li>/gi;
|
||||||
|
let itemMatch;
|
||||||
|
|
||||||
|
while ((itemMatch = itemRegex.exec(listContent)) !== null) {
|
||||||
|
const itemText = this.stripHtml(itemMatch[1]).trim();
|
||||||
|
if (itemText) {
|
||||||
|
items.push(itemText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract tables from HTML content
|
||||||
|
*/
|
||||||
|
private extractTables(content: string): Array<{ headers: string[], rows: string[][] }> {
|
||||||
|
const tables = [];
|
||||||
|
const tableRegex = /<table[^>]*>([\s\S]*?)<\/table>/gi;
|
||||||
|
let tableMatch;
|
||||||
|
|
||||||
|
while ((tableMatch = tableRegex.exec(content)) !== null) {
|
||||||
|
const tableContent = tableMatch[1];
|
||||||
|
const headers = [];
|
||||||
|
const rows = [];
|
||||||
|
|
||||||
|
// Extract table headers
|
||||||
|
const headerRegex = /<th[^>]*>([\s\S]*?)<\/th>/gi;
|
||||||
|
let headerMatch;
|
||||||
|
while ((headerMatch = headerRegex.exec(tableContent)) !== null) {
|
||||||
|
headers.push(this.stripHtml(headerMatch[1]).trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract table rows
|
||||||
|
const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
|
||||||
|
let rowMatch;
|
||||||
|
while ((rowMatch = rowRegex.exec(tableContent)) !== null) {
|
||||||
|
const rowContent = rowMatch[1];
|
||||||
|
const cells = [];
|
||||||
|
|
||||||
|
const cellRegex = /<td[^>]*>([\s\S]*?)<\/td>/gi;
|
||||||
|
let cellMatch;
|
||||||
|
while ((cellMatch = cellRegex.exec(rowContent)) !== null) {
|
||||||
|
cells.push(this.stripHtml(cellMatch[1]).trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cells.length > 0) {
|
||||||
|
rows.push(cells);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headers.length > 0 || rows.length > 0) {
|
||||||
|
tables.push({
|
||||||
|
headers,
|
||||||
|
rows
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tables;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract headings from HTML content
|
||||||
|
*/
|
||||||
|
private extractHeadings(content: string): Array<{ level: number, text: string }> {
|
||||||
|
const headings = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= 6; i++) {
|
||||||
|
const headingRegex = new RegExp(`<h${i}[^>]*>([\s\S]*?)<\/h${i}>`, 'gi');
|
||||||
|
let headingMatch;
|
||||||
|
|
||||||
|
while ((headingMatch = headingRegex.exec(content)) !== null) {
|
||||||
|
const headingText = this.stripHtml(headingMatch[1]).trim();
|
||||||
|
if (headingText) {
|
||||||
|
headings.push({
|
||||||
|
level: i,
|
||||||
|
text: headingText
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return headings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract code blocks from HTML content
|
||||||
|
*/
|
||||||
|
private extractCodeBlocks(content: string): Array<{ language?: string, code: string }> {
|
||||||
|
const codeBlocks = [];
|
||||||
|
|
||||||
|
// Look for <pre> and <code> blocks
|
||||||
|
const preRegex = /<pre[^>]*>([\s\S]*?)<\/pre>/gi;
|
||||||
|
let preMatch;
|
||||||
|
|
||||||
|
while ((preMatch = preRegex.exec(content)) !== null) {
|
||||||
|
const preContent = preMatch[1];
|
||||||
|
// Check if there's a nested <code> tag
|
||||||
|
const codeMatch = /<code[^>]*>([\s\S]*?)<\/code>/i.exec(preContent);
|
||||||
|
|
||||||
|
if (codeMatch) {
|
||||||
|
// Extract language if it's in the class attribute
|
||||||
|
const classMatch = /class="[^"]*language-([^"\s]+)[^"]*"/i.exec(preMatch[0]);
|
||||||
|
codeBlocks.push({
|
||||||
|
language: classMatch ? classMatch[1] : undefined,
|
||||||
|
code: this.decodeHtmlEntities(codeMatch[1]).trim()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Just a <pre> without <code>
|
||||||
|
codeBlocks.push({
|
||||||
|
code: this.decodeHtmlEntities(preContent).trim()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also look for standalone <code> blocks not inside <pre>
|
||||||
|
const standaloneCodeRegex = /(?<!<pre[^>]*>[\s\S]*?)<code[^>]*>([\s\S]*?)<\/code>/gi;
|
||||||
|
let standaloneCodeMatch;
|
||||||
|
|
||||||
|
while ((standaloneCodeMatch = standaloneCodeRegex.exec(content)) !== null) {
|
||||||
|
codeBlocks.push({
|
||||||
|
code: this.decodeHtmlEntities(standaloneCodeMatch[1]).trim()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return codeBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter content by query
|
||||||
|
*/
|
||||||
|
private filterContentByQuery(content: any, query: string): void {
|
||||||
|
const lowerQuery = query.toLowerCase();
|
||||||
|
|
||||||
|
// Filter lists
|
||||||
|
if (content.lists) {
|
||||||
|
content.lists = content.lists.filter(list => {
|
||||||
|
// Keep the list if any item matches the query
|
||||||
|
return list.items.some(item => item.toLowerCase().includes(lowerQuery));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also filter individual items in each list
|
||||||
|
content.lists.forEach(list => {
|
||||||
|
list.items = list.items.filter(item => item.toLowerCase().includes(lowerQuery));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter headings
|
||||||
|
if (content.headings) {
|
||||||
|
content.headings = content.headings.filter(heading =>
|
||||||
|
heading.text.toLowerCase().includes(lowerQuery)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter tables
|
||||||
|
if (content.tables) {
|
||||||
|
content.tables = content.tables.filter(table => {
|
||||||
|
// Check headers
|
||||||
|
const headerMatch = table.headers.some(header =>
|
||||||
|
header.toLowerCase().includes(lowerQuery)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check cells
|
||||||
|
const cellMatch = table.rows.some(row =>
|
||||||
|
row.some(cell => cell.toLowerCase().includes(lowerQuery))
|
||||||
|
);
|
||||||
|
|
||||||
|
return headerMatch || cellMatch;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter code blocks
|
||||||
|
if (content.codeBlocks) {
|
||||||
|
content.codeBlocks = content.codeBlocks.filter(block =>
|
||||||
|
block.code.toLowerCase().includes(lowerQuery)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format extracted content as Markdown
|
||||||
|
*/
|
||||||
|
private formatAsMarkdown(content: any, extractionType: string): string {
|
||||||
|
let markdown = '';
|
||||||
|
|
||||||
|
if (extractionType === 'lists' || extractionType === 'all') {
|
||||||
|
if (content.lists && content.lists.length > 0) {
|
||||||
|
markdown += '## Lists\n\n';
|
||||||
|
|
||||||
|
content.lists.forEach((list: any, index: number) => {
|
||||||
|
markdown += `### List ${index + 1} (${list.type})\n\n`;
|
||||||
|
|
||||||
|
list.items.forEach((item: string) => {
|
||||||
|
if (list.type === 'unordered') {
|
||||||
|
markdown += `- ${item}\n`;
|
||||||
|
} else {
|
||||||
|
markdown += `1. ${item}\n`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
markdown += '\n';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extractionType === 'headings' || extractionType === 'all') {
|
||||||
|
if (content.headings && content.headings.length > 0) {
|
||||||
|
markdown += '## Headings\n\n';
|
||||||
|
|
||||||
|
content.headings.forEach((heading: any) => {
|
||||||
|
markdown += `${'#'.repeat(heading.level)} ${heading.text}\n\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extractionType === 'tables' || extractionType === 'all') {
|
||||||
|
if (content.tables && content.tables.length > 0) {
|
||||||
|
markdown += '## Tables\n\n';
|
||||||
|
|
||||||
|
content.tables.forEach((table: any, index: number) => {
|
||||||
|
markdown += `### Table ${index + 1}\n\n`;
|
||||||
|
|
||||||
|
// Add headers
|
||||||
|
if (table.headers.length > 0) {
|
||||||
|
markdown += '| ' + table.headers.join(' | ') + ' |\n';
|
||||||
|
markdown += '| ' + table.headers.map(() => '---').join(' | ') + ' |\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add rows
|
||||||
|
table.rows.forEach((row: string[]) => {
|
||||||
|
markdown += '| ' + row.join(' | ') + ' |\n';
|
||||||
|
});
|
||||||
|
|
||||||
|
markdown += '\n';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extractionType === 'codeBlocks' || extractionType === 'all') {
|
||||||
|
if (content.codeBlocks && content.codeBlocks.length > 0) {
|
||||||
|
markdown += '## Code Blocks\n\n';
|
||||||
|
|
||||||
|
content.codeBlocks.forEach((block: any, index: number) => {
|
||||||
|
markdown += `### Code Block ${index + 1}\n\n`;
|
||||||
|
|
||||||
|
if (block.language) {
|
||||||
|
markdown += '```' + block.language + '\n';
|
||||||
|
} else {
|
||||||
|
markdown += '```\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
markdown += block.code + '\n';
|
||||||
|
markdown += '```\n\n';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return markdown.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format extracted content as plain text
|
||||||
|
*/
|
||||||
|
private formatAsText(content: any, extractionType: string): string {
|
||||||
|
let text = '';
|
||||||
|
|
||||||
|
if (extractionType === 'lists' || extractionType === 'all') {
|
||||||
|
if (content.lists && content.lists.length > 0) {
|
||||||
|
text += 'LISTS:\n\n';
|
||||||
|
|
||||||
|
content.lists.forEach((list: any, index: number) => {
|
||||||
|
text += `List ${index + 1} (${list.type}):\n\n`;
|
||||||
|
|
||||||
|
list.items.forEach((item: string, itemIndex: number) => {
|
||||||
|
if (list.type === 'unordered') {
|
||||||
|
text += `• ${item}\n`;
|
||||||
|
} else {
|
||||||
|
text += `${itemIndex + 1}. ${item}\n`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
text += '\n';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extractionType === 'headings' || extractionType === 'all') {
|
||||||
|
if (content.headings && content.headings.length > 0) {
|
||||||
|
text += 'HEADINGS:\n\n';
|
||||||
|
|
||||||
|
content.headings.forEach((heading: any) => {
|
||||||
|
text += `${heading.text} (Level ${heading.level})\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
text += '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extractionType === 'tables' || extractionType === 'all') {
|
||||||
|
if (content.tables && content.tables.length > 0) {
|
||||||
|
text += 'TABLES:\n\n';
|
||||||
|
|
||||||
|
content.tables.forEach((table: any, index: number) => {
|
||||||
|
text += `Table ${index + 1}:\n\n`;
|
||||||
|
|
||||||
|
// Add headers
|
||||||
|
if (table.headers.length > 0) {
|
||||||
|
text += table.headers.join(' | ') + '\n';
|
||||||
|
text += table.headers.map(() => '-----').join(' | ') + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add rows
|
||||||
|
table.rows.forEach((row: string[]) => {
|
||||||
|
text += row.join(' | ') + '\n';
|
||||||
|
});
|
||||||
|
|
||||||
|
text += '\n';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extractionType === 'codeBlocks' || extractionType === 'all') {
|
||||||
|
if (content.codeBlocks && content.codeBlocks.length > 0) {
|
||||||
|
text += 'CODE BLOCKS:\n\n';
|
||||||
|
|
||||||
|
content.codeBlocks.forEach((block: any, index: number) => {
|
||||||
|
text += `Code Block ${index + 1}`;
|
||||||
|
|
||||||
|
if (block.language) {
|
||||||
|
text += ` (${block.language})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
text += ':\n\n';
|
||||||
|
text += block.code + '\n\n';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip HTML tags from content
|
||||||
|
*/
|
||||||
|
private stripHtml(html: string): string {
|
||||||
|
return html.replace(/<[^>]*>/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode HTML entities
|
||||||
|
*/
|
||||||
|
private decodeHtmlEntities(text: string): string {
|
||||||
|
return text
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/ /g, ' ');
|
||||||
|
}
|
||||||
|
}
|
170
src/services/llm/tools/note_creation_tool.ts
Normal file
170
src/services/llm/tools/note_creation_tool.ts
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
/**
|
||||||
|
* Note Creation Tool
|
||||||
|
*
|
||||||
|
* This tool allows the LLM to create new notes in Trilium.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Tool, ToolHandler } from './tool_interfaces.js';
|
||||||
|
import log from '../../log.js';
|
||||||
|
import becca from '../../../becca/becca.js';
|
||||||
|
import notes from '../../notes.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition of the note creation tool
|
||||||
|
*/
|
||||||
|
export const noteCreationToolDefinition: Tool = {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'create_note',
|
||||||
|
description: 'Create a new note in Trilium with the specified content and attributes',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
parentNoteId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID of the parent note under which to create the new note. If not specified, creates under root.'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Title of the new note'
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Content of the new note'
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Type of the note (text, code, etc.)',
|
||||||
|
enum: ['text', 'code', 'file', 'image', 'search', 'relation-map', 'book', 'mermaid', 'canvas']
|
||||||
|
},
|
||||||
|
mime: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'MIME type of the note (e.g., text/html, application/json). Only required for certain note types.'
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
type: 'array',
|
||||||
|
description: 'Array of attributes to set on the note (e.g., [{"name":"#tag"}, {"name":"priority", "value":"high"}])',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Name of the attribute'
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Value of the attribute (if applicable)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['title', 'content']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note creation tool implementation
|
||||||
|
*/
|
||||||
|
export class NoteCreationTool implements ToolHandler {
|
||||||
|
public definition: Tool = noteCreationToolDefinition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the note creation tool
|
||||||
|
*/
|
||||||
|
public async execute(args: {
|
||||||
|
parentNoteId?: string,
|
||||||
|
title: string,
|
||||||
|
content: string,
|
||||||
|
type?: string,
|
||||||
|
mime?: string,
|
||||||
|
attributes?: Array<{ name: string, value?: string }>
|
||||||
|
}): Promise<string | object> {
|
||||||
|
try {
|
||||||
|
const { parentNoteId, title, content, type = 'text', mime } = args;
|
||||||
|
|
||||||
|
log.info(`Executing create_note tool - Title: "${title}", Type: ${type}, ParentNoteId: ${parentNoteId || 'root'}`);
|
||||||
|
|
||||||
|
// Validate parent note exists if specified
|
||||||
|
let parent;
|
||||||
|
if (parentNoteId) {
|
||||||
|
parent = becca.notes[parentNoteId];
|
||||||
|
if (!parent) {
|
||||||
|
return `Error: Parent note with ID ${parentNoteId} not found. Please specify a valid parent note ID.`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use root note if no parent specified
|
||||||
|
parent = becca.getNote('root');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the appropriate mime type
|
||||||
|
let noteMime = mime;
|
||||||
|
if (!noteMime) {
|
||||||
|
// Set default mime types based on note type
|
||||||
|
switch (type) {
|
||||||
|
case 'text':
|
||||||
|
noteMime = 'text/html';
|
||||||
|
break;
|
||||||
|
case 'code':
|
||||||
|
noteMime = 'text/plain';
|
||||||
|
break;
|
||||||
|
case 'file':
|
||||||
|
noteMime = 'application/octet-stream';
|
||||||
|
break;
|
||||||
|
case 'image':
|
||||||
|
noteMime = 'image/png';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
noteMime = 'text/html';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the note
|
||||||
|
const createStartTime = Date.now();
|
||||||
|
const noteId = await notes.createNewNote({
|
||||||
|
parentNoteId: parent.noteId,
|
||||||
|
title: title,
|
||||||
|
content: content,
|
||||||
|
type: type,
|
||||||
|
mime: noteMime
|
||||||
|
});
|
||||||
|
const createDuration = Date.now() - createStartTime;
|
||||||
|
|
||||||
|
if (!noteId) {
|
||||||
|
return 'Error: Failed to create note. An unknown error occurred.';
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Note created successfully in ${createDuration}ms, ID: ${noteId}`);
|
||||||
|
|
||||||
|
// Add attributes if specified
|
||||||
|
if (args.attributes && args.attributes.length > 0) {
|
||||||
|
log.info(`Adding ${args.attributes.length} attributes to the note`);
|
||||||
|
|
||||||
|
for (const attr of args.attributes) {
|
||||||
|
if (!attr.name) continue;
|
||||||
|
|
||||||
|
const attrStartTime = Date.now();
|
||||||
|
await notes.createAttribute(noteId, attr.name, attr.value || '');
|
||||||
|
const attrDuration = Date.now() - attrStartTime;
|
||||||
|
|
||||||
|
log.info(`Added attribute ${attr.name}=${attr.value || ''} in ${attrDuration}ms`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the new note's information
|
||||||
|
const newNote = becca.notes[noteId];
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
noteId: noteId,
|
||||||
|
title: newNote.title,
|
||||||
|
type: newNote.type,
|
||||||
|
message: `Note "${title}" created successfully`
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error executing create_note tool: ${error.message || String(error)}`);
|
||||||
|
return `Error: ${error.message || String(error)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
185
src/services/llm/tools/note_summarization_tool.ts
Normal file
185
src/services/llm/tools/note_summarization_tool.ts
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
/**
|
||||||
|
* Note Summarization Tool
|
||||||
|
*
|
||||||
|
* This tool allows the LLM to generate concise summaries of longer notes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Tool, ToolHandler } from './tool_interfaces.js';
|
||||||
|
import log from '../../log.js';
|
||||||
|
import becca from '../../../becca/becca.js';
|
||||||
|
import aiServiceManager from '../ai_service_manager.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition of the note summarization tool
|
||||||
|
*/
|
||||||
|
export const noteSummarizationToolDefinition: Tool = {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'summarize_note',
|
||||||
|
description: 'Generate a concise summary of a note\'s content',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
noteId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID of the note to summarize'
|
||||||
|
},
|
||||||
|
maxLength: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Maximum length of the summary in characters (default: 500)'
|
||||||
|
},
|
||||||
|
format: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Format of the summary',
|
||||||
|
enum: ['paragraph', 'bullets', 'executive']
|
||||||
|
},
|
||||||
|
focus: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Optional focus for the summary (e.g., "technical details", "key findings")'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['noteId']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note summarization tool implementation
|
||||||
|
*/
|
||||||
|
export class NoteSummarizationTool implements ToolHandler {
|
||||||
|
public definition: Tool = noteSummarizationToolDefinition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the note summarization tool
|
||||||
|
*/
|
||||||
|
public async execute(args: {
|
||||||
|
noteId: string,
|
||||||
|
maxLength?: number,
|
||||||
|
format?: 'paragraph' | 'bullets' | 'executive',
|
||||||
|
focus?: string
|
||||||
|
}): Promise<string | object> {
|
||||||
|
try {
|
||||||
|
const { noteId, maxLength = 500, format = 'paragraph', focus } = args;
|
||||||
|
|
||||||
|
log.info(`Executing summarize_note tool - NoteID: "${noteId}", MaxLength: ${maxLength}, Format: ${format}`);
|
||||||
|
|
||||||
|
// 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 the note content
|
||||||
|
const content = await note.getContent();
|
||||||
|
|
||||||
|
if (!content || typeof content !== 'string' || content.trim().length === 0) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Note content is empty or invalid'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Retrieved note content, length: ${content.length} chars`);
|
||||||
|
|
||||||
|
// Check if content needs summarization (if it's short enough, just return it)
|
||||||
|
if (content.length <= maxLength && !focus) {
|
||||||
|
log.info(`Note content is already shorter than maxLength, returning as is`);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
noteId: note.noteId,
|
||||||
|
title: note.title,
|
||||||
|
summary: this.cleanHtml(content),
|
||||||
|
wasAlreadyShort: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove HTML tags for summarization
|
||||||
|
const cleanContent = this.cleanHtml(content);
|
||||||
|
|
||||||
|
// Generate the summary using the AI service
|
||||||
|
const aiService = aiServiceManager.getService();
|
||||||
|
|
||||||
|
if (!aiService) {
|
||||||
|
log.error('No AI service available for summarization');
|
||||||
|
return `Error: No AI service is available for summarization`;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Using ${aiService.getName()} to generate summary`);
|
||||||
|
|
||||||
|
// Create a prompt based on format and focus
|
||||||
|
let prompt = `Summarize the following text`;
|
||||||
|
|
||||||
|
if (focus) {
|
||||||
|
prompt += ` with a focus on ${focus}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format === 'bullets') {
|
||||||
|
prompt += ` in a bullet point format`;
|
||||||
|
} else if (format === 'executive') {
|
||||||
|
prompt += ` as a brief executive summary`;
|
||||||
|
} else {
|
||||||
|
prompt += ` in a concise paragraph`;
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt += `. Keep the summary under ${maxLength} characters:\n\n${cleanContent}`;
|
||||||
|
|
||||||
|
// Generate the summary
|
||||||
|
const summaryStartTime = Date.now();
|
||||||
|
|
||||||
|
const completion = await aiService.generateChatCompletion([
|
||||||
|
{ role: 'system', content: 'You are a skilled summarizer. Create concise, accurate summaries while preserving the key information.' },
|
||||||
|
{ role: 'user', content: prompt }
|
||||||
|
], {
|
||||||
|
temperature: 0.3, // Lower temperature for more focused summaries
|
||||||
|
maxTokens: 1000 // Enough tokens for the summary
|
||||||
|
});
|
||||||
|
|
||||||
|
const summaryDuration = Date.now() - summaryStartTime;
|
||||||
|
|
||||||
|
log.info(`Generated summary in ${summaryDuration}ms, length: ${completion.text.length} chars`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
noteId: note.noteId,
|
||||||
|
title: note.title,
|
||||||
|
originalLength: content.length,
|
||||||
|
summary: completion.text,
|
||||||
|
format: format,
|
||||||
|
focus: focus || 'general content'
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error executing summarize_note tool: ${error.message || String(error)}`);
|
||||||
|
return `Error: ${error.message || String(error)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean HTML content for summarization
|
||||||
|
*/
|
||||||
|
private cleanHtml(html: string): string {
|
||||||
|
if (typeof html !== 'string') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove HTML tags
|
||||||
|
let text = html.replace(/<[^>]*>/g, '');
|
||||||
|
|
||||||
|
// Decode common HTML entities
|
||||||
|
text = text
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/ /g, ' ');
|
||||||
|
|
||||||
|
// Normalize whitespace
|
||||||
|
text = text.replace(/\s+/g, ' ').trim();
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
136
src/services/llm/tools/note_update_tool.ts
Normal file
136
src/services/llm/tools/note_update_tool.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* Note Update Tool
|
||||||
|
*
|
||||||
|
* This tool allows the LLM to update existing notes in Trilium.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Tool, ToolHandler } from './tool_interfaces.js';
|
||||||
|
import log from '../../log.js';
|
||||||
|
import becca from '../../../becca/becca.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition of the note update tool
|
||||||
|
*/
|
||||||
|
export const noteUpdateToolDefinition: Tool = {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'update_note',
|
||||||
|
description: 'Update the content or title of an existing note',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
noteId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID of the note to update'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'New title for the note (if you want to change it)'
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'New content for the note (if you want to change it)'
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'How to update content: replace (default), append, or prepend',
|
||||||
|
enum: ['replace', 'append', 'prepend']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['noteId']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note update tool implementation
|
||||||
|
*/
|
||||||
|
export class NoteUpdateTool implements ToolHandler {
|
||||||
|
public definition: Tool = noteUpdateToolDefinition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the note update tool
|
||||||
|
*/
|
||||||
|
public async execute(args: { noteId: string, title?: string, content?: string, mode?: 'replace' | 'append' | 'prepend' }): Promise<string | object> {
|
||||||
|
try {
|
||||||
|
const { noteId, title, content, mode = 'replace' } = args;
|
||||||
|
|
||||||
|
if (!title && !content) {
|
||||||
|
return 'Error: At least one of title or content must be provided to update a note.';
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Executing update_note tool - NoteID: "${noteId}", Mode: ${mode}`);
|
||||||
|
|
||||||
|
// 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})`);
|
||||||
|
|
||||||
|
let titleUpdateResult;
|
||||||
|
let contentUpdateResult;
|
||||||
|
|
||||||
|
// Update title if provided
|
||||||
|
if (title && title !== note.title) {
|
||||||
|
const titleStartTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await note.setTitle(title);
|
||||||
|
const titleDuration = Date.now() - titleStartTime;
|
||||||
|
log.info(`Updated note title to "${title}" in ${titleDuration}ms`);
|
||||||
|
titleUpdateResult = `Title updated from "${note.title}" to "${title}"`;
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error updating note title: ${error.message || String(error)}`);
|
||||||
|
titleUpdateResult = `Failed to update title: ${error.message || 'Unknown error'}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update content if provided
|
||||||
|
if (content) {
|
||||||
|
const contentStartTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
let newContent = content;
|
||||||
|
|
||||||
|
// For append or prepend modes, get the current content first
|
||||||
|
if (mode === 'append' || mode === 'prepend') {
|
||||||
|
const currentContent = await note.getContent();
|
||||||
|
|
||||||
|
if (mode === 'append') {
|
||||||
|
newContent = currentContent + '\n\n' + content;
|
||||||
|
log.info(`Appending content to existing note content`);
|
||||||
|
} else if (mode === 'prepend') {
|
||||||
|
newContent = content + '\n\n' + currentContent;
|
||||||
|
log.info(`Prepending content to existing note content`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await note.setContent(newContent);
|
||||||
|
const contentDuration = Date.now() - contentStartTime;
|
||||||
|
log.info(`Updated note content in ${contentDuration}ms, new content length: ${newContent.length}`);
|
||||||
|
contentUpdateResult = `Content updated successfully (${mode} mode)`;
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error updating note content: ${error.message || String(error)}`);
|
||||||
|
contentUpdateResult = `Failed to update content: ${error.message || 'Unknown error'}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the results
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
noteId: note.noteId,
|
||||||
|
title: note.title,
|
||||||
|
titleUpdate: titleUpdateResult || 'No title update requested',
|
||||||
|
contentUpdate: contentUpdateResult || 'No content update requested',
|
||||||
|
message: `Note "${note.title}" updated successfully`
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error executing update_note tool: ${error.message || String(error)}`);
|
||||||
|
return `Error: ${error.message || String(error)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
382
src/services/llm/tools/relationship_tool.ts
Normal file
382
src/services/llm/tools/relationship_tool.ts
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
/**
|
||||||
|
* Relationship Tool
|
||||||
|
*
|
||||||
|
* This tool allows the LLM to create, identify, or modify relationships between notes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Tool, ToolHandler } from './tool_interfaces.js';
|
||||||
|
import log from '../../log.js';
|
||||||
|
import becca from '../../../becca/becca.js';
|
||||||
|
import attributes from '../../attributes.js';
|
||||||
|
import aiServiceManager from '../ai_service_manager.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition of the relationship tool
|
||||||
|
*/
|
||||||
|
export const relationshipToolDefinition: Tool = {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'manage_relationships',
|
||||||
|
description: 'Create, list, or modify relationships between notes',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
action: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Action to perform on relationships',
|
||||||
|
enum: ['create', 'list', 'find_related', 'suggest']
|
||||||
|
},
|
||||||
|
sourceNoteId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID of the source note for the relationship'
|
||||||
|
},
|
||||||
|
targetNoteId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID of the target note for the relationship (for create action)'
|
||||||
|
},
|
||||||
|
relationName: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Name of the relation (for create action, e.g., "references", "belongs to", "depends on")'
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Maximum number of relationships to return (for list action)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['action', 'sourceNoteId']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relationship tool implementation
|
||||||
|
*/
|
||||||
|
export class RelationshipTool implements ToolHandler {
|
||||||
|
public definition: Tool = relationshipToolDefinition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the relationship tool
|
||||||
|
*/
|
||||||
|
public async execute(args: {
|
||||||
|
action: 'create' | 'list' | 'find_related' | 'suggest',
|
||||||
|
sourceNoteId: string,
|
||||||
|
targetNoteId?: string,
|
||||||
|
relationName?: string,
|
||||||
|
limit?: number
|
||||||
|
}): Promise<string | object> {
|
||||||
|
try {
|
||||||
|
const { action, sourceNoteId, targetNoteId, relationName, limit = 10 } = args;
|
||||||
|
|
||||||
|
log.info(`Executing manage_relationships tool - Action: ${action}, SourceNoteId: ${sourceNoteId}`);
|
||||||
|
|
||||||
|
// Get the source note from becca
|
||||||
|
const sourceNote = becca.notes[sourceNoteId];
|
||||||
|
|
||||||
|
if (!sourceNote) {
|
||||||
|
log.info(`Source note with ID ${sourceNoteId} not found - returning error`);
|
||||||
|
return `Error: Source note with ID ${sourceNoteId} not found`;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Found source note: "${sourceNote.title}" (Type: ${sourceNote.type})`);
|
||||||
|
|
||||||
|
// Handle different actions
|
||||||
|
if (action === 'create') {
|
||||||
|
return await this.createRelationship(sourceNote, targetNoteId, relationName);
|
||||||
|
} else if (action === 'list') {
|
||||||
|
return await this.listRelationships(sourceNote, limit);
|
||||||
|
} else if (action === 'find_related') {
|
||||||
|
return await this.findRelatedNotes(sourceNote, limit);
|
||||||
|
} else if (action === 'suggest') {
|
||||||
|
return await this.suggestRelationships(sourceNote, limit);
|
||||||
|
} else {
|
||||||
|
return `Error: Unsupported action "${action}". Supported actions are: create, list, find_related, suggest`;
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error executing manage_relationships tool: ${error.message || String(error)}`);
|
||||||
|
return `Error: ${error.message || String(error)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a relationship between notes
|
||||||
|
*/
|
||||||
|
private async createRelationship(sourceNote: any, targetNoteId?: string, relationName?: string): Promise<object> {
|
||||||
|
if (!targetNoteId) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Target note ID is required for create action'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!relationName) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Relation name is required for create action'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the target note from becca
|
||||||
|
const targetNote = becca.notes[targetNoteId];
|
||||||
|
|
||||||
|
if (!targetNote) {
|
||||||
|
log.info(`Target note with ID ${targetNoteId} not found - returning error`);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Target note with ID ${targetNoteId} not found`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Found target note: "${targetNote.title}" (Type: ${targetNote.type})`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if relationship already exists
|
||||||
|
const existingRelations = sourceNote.getRelationTargets(relationName);
|
||||||
|
|
||||||
|
for (const existingNote of existingRelations) {
|
||||||
|
if (existingNote.noteId === targetNoteId) {
|
||||||
|
log.info(`Relationship ${relationName} already exists from "${sourceNote.title}" to "${targetNote.title}"`);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
sourceNoteId: sourceNote.noteId,
|
||||||
|
sourceTitle: sourceNote.title,
|
||||||
|
targetNoteId: targetNote.noteId,
|
||||||
|
targetTitle: targetNote.title,
|
||||||
|
relationName: relationName,
|
||||||
|
message: `Relationship ${relationName} already exists from "${sourceNote.title}" to "${targetNote.title}"`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the relationship attribute
|
||||||
|
const startTime = Date.now();
|
||||||
|
await attributes.createRelation(sourceNote.noteId, relationName, targetNote.noteId);
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
log.info(`Created relationship ${relationName} from "${sourceNote.title}" to "${targetNote.title}" in ${duration}ms`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
sourceNoteId: sourceNote.noteId,
|
||||||
|
sourceTitle: sourceNote.title,
|
||||||
|
targetNoteId: targetNote.noteId,
|
||||||
|
targetTitle: targetNote.title,
|
||||||
|
relationName: relationName,
|
||||||
|
message: `Created relationship ${relationName} from "${sourceNote.title}" to "${targetNote.title}"`
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error creating relationship: ${error.message || String(error)}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List relationships for a note
|
||||||
|
*/
|
||||||
|
private async listRelationships(sourceNote: any, limit: number): Promise<object> {
|
||||||
|
try {
|
||||||
|
// Get outgoing relationships (where this note is the source)
|
||||||
|
const outgoingAttributes = sourceNote.getAttributes()
|
||||||
|
.filter((attr: any) => attr.type === 'relation')
|
||||||
|
.slice(0, limit);
|
||||||
|
|
||||||
|
const outgoingRelations = [];
|
||||||
|
|
||||||
|
for (const attr of outgoingAttributes) {
|
||||||
|
const targetNote = becca.notes[attr.value];
|
||||||
|
|
||||||
|
if (targetNote) {
|
||||||
|
outgoingRelations.push({
|
||||||
|
relationName: attr.name,
|
||||||
|
targetNoteId: targetNote.noteId,
|
||||||
|
targetTitle: targetNote.title
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get incoming relationships (where this note is the target)
|
||||||
|
const incomingNotes = becca.findNotesWithRelation(sourceNote.noteId);
|
||||||
|
const incomingRelations = [];
|
||||||
|
|
||||||
|
for (const sourceOfRelation of incomingNotes) {
|
||||||
|
const incomingAttributes = sourceOfRelation.getOwnedAttributes()
|
||||||
|
.filter((attr: any) => attr.type === 'relation' && attr.value === sourceNote.noteId);
|
||||||
|
|
||||||
|
for (const attr of incomingAttributes) {
|
||||||
|
incomingRelations.push({
|
||||||
|
relationName: attr.name,
|
||||||
|
sourceNoteId: sourceOfRelation.noteId,
|
||||||
|
sourceTitle: sourceOfRelation.title
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (incomingRelations.length >= limit) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Found ${outgoingRelations.length} outgoing and ${incomingRelations.length} incoming relationships`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
noteId: sourceNote.noteId,
|
||||||
|
title: sourceNote.title,
|
||||||
|
outgoingRelations: outgoingRelations,
|
||||||
|
incomingRelations: incomingRelations.slice(0, limit),
|
||||||
|
message: `Found ${outgoingRelations.length} outgoing and ${incomingRelations.length} incoming relationships for "${sourceNote.title}"`
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error listing relationships: ${error.message || String(error)}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find related notes using vector similarity
|
||||||
|
*/
|
||||||
|
private async findRelatedNotes(sourceNote: any, limit: number): Promise<object> {
|
||||||
|
try {
|
||||||
|
// Get the vector search tool from the AI service manager
|
||||||
|
const vectorSearchTool = aiServiceManager.getVectorSearchTool();
|
||||||
|
|
||||||
|
if (!vectorSearchTool) {
|
||||||
|
log.error('Vector search tool not available');
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Vector search capability not available'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Using vector search to find notes related to "${sourceNote.title}"`);
|
||||||
|
|
||||||
|
// Get note content for semantic search
|
||||||
|
const content = await sourceNote.getContent();
|
||||||
|
const title = sourceNote.title;
|
||||||
|
|
||||||
|
// Use both title and content for search
|
||||||
|
const searchQuery = title + (content && typeof content === 'string' ? ': ' + content.substring(0, 500) : '');
|
||||||
|
|
||||||
|
// Execute the search
|
||||||
|
const searchStartTime = Date.now();
|
||||||
|
const results = await vectorSearchTool.searchNotes(searchQuery, {
|
||||||
|
maxResults: limit + 1 // Add 1 to account for the source note itself
|
||||||
|
});
|
||||||
|
const searchDuration = Date.now() - searchStartTime;
|
||||||
|
|
||||||
|
// Filter out the source note from results
|
||||||
|
const filteredResults = results.filter(note => note.noteId !== sourceNote.noteId);
|
||||||
|
log.info(`Found ${filteredResults.length} related notes in ${searchDuration}ms`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
noteId: sourceNote.noteId,
|
||||||
|
title: sourceNote.title,
|
||||||
|
relatedNotes: filteredResults.slice(0, limit).map(note => ({
|
||||||
|
noteId: note.noteId,
|
||||||
|
title: note.title,
|
||||||
|
similarity: Math.round(note.similarity * 100) / 100
|
||||||
|
})),
|
||||||
|
message: `Found ${filteredResults.length} notes semantically related to "${sourceNote.title}"`
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error finding related notes: ${error.message || String(error)}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suggest possible relationships based on content analysis
|
||||||
|
*/
|
||||||
|
private async suggestRelationships(sourceNote: any, limit: number): Promise<object> {
|
||||||
|
try {
|
||||||
|
// First, find related notes using vector search
|
||||||
|
const relatedResult = await this.findRelatedNotes(sourceNote, limit) as any;
|
||||||
|
|
||||||
|
if (!relatedResult.success || !relatedResult.relatedNotes || relatedResult.relatedNotes.length === 0) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Could not find any related notes to suggest relationships'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the AI service for relationship suggestion
|
||||||
|
const aiService = aiServiceManager.getService();
|
||||||
|
|
||||||
|
if (!aiService) {
|
||||||
|
log.error('No AI service available for relationship suggestions');
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'AI service not available for relationship suggestions',
|
||||||
|
relatedNotes: relatedResult.relatedNotes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Using ${aiService.getName()} to suggest relationships for ${relatedResult.relatedNotes.length} related notes`);
|
||||||
|
|
||||||
|
// Get the source note content
|
||||||
|
const sourceContent = await sourceNote.getContent();
|
||||||
|
|
||||||
|
// Prepare suggestions
|
||||||
|
const suggestions = [];
|
||||||
|
|
||||||
|
for (const relatedNote of relatedResult.relatedNotes) {
|
||||||
|
try {
|
||||||
|
// Get the target note content
|
||||||
|
const targetNote = becca.notes[relatedNote.noteId];
|
||||||
|
const targetContent = await targetNote.getContent();
|
||||||
|
|
||||||
|
// Prepare a prompt for the AI service
|
||||||
|
const prompt = `Analyze the relationship between these two notes and suggest a descriptive relation name (like "references", "implements", "depends on", etc.)
|
||||||
|
|
||||||
|
SOURCE NOTE: "${sourceNote.title}"
|
||||||
|
${typeof sourceContent === 'string' ? sourceContent.substring(0, 300) : ''}
|
||||||
|
|
||||||
|
TARGET NOTE: "${targetNote.title}"
|
||||||
|
${typeof targetContent === 'string' ? targetContent.substring(0, 300) : ''}
|
||||||
|
|
||||||
|
Suggest the most appropriate relationship type that would connect the source note to the target note. Reply with ONLY the relationship name, nothing else.`;
|
||||||
|
|
||||||
|
// Get the suggestion
|
||||||
|
const completion = await aiService.generateChatCompletion([
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: 'You analyze the relationship between notes and suggest a concise, descriptive relation name.'
|
||||||
|
},
|
||||||
|
{ role: 'user', content: prompt }
|
||||||
|
], {
|
||||||
|
temperature: 0.4,
|
||||||
|
maxTokens: 50
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract just the relation name (remove any formatting or explanation)
|
||||||
|
const relationName = completion.text
|
||||||
|
.replace(/^["']|["']$/g, '') // Remove quotes
|
||||||
|
.replace(/^relationship:|\./gi, '') // Remove prefixes/suffixes
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
suggestions.push({
|
||||||
|
targetNoteId: relatedNote.noteId,
|
||||||
|
targetTitle: relatedNote.title,
|
||||||
|
similarity: relatedNote.similarity,
|
||||||
|
suggestedRelation: relationName
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info(`Suggested relationship "${relationName}" from "${sourceNote.title}" to "${targetNote.title}"`);
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error generating suggestion: ${error.message || String(error)}`);
|
||||||
|
// Continue with other suggestions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
noteId: sourceNote.noteId,
|
||||||
|
title: sourceNote.title,
|
||||||
|
suggestions: suggestions,
|
||||||
|
message: `Generated ${suggestions.length} relationship suggestions for "${sourceNote.title}"`
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
log.error(`Error suggesting relationships: ${error.message || String(error)}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user