mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2026-01-08 10:12:44 -08:00
* Add watch-filesystem-adaptor plugin and worker IPC Introduces the watch-filesystem-adaptor TiddlyWiki plugin, enabling tag-based routing of tiddlers to sub-wikis by querying workspace info via worker thread IPC. Adds workerServiceCaller utility for worker-to-main service calls, updates workerAdapter and bindServiceAndProxy to support explicit service registration for workers, and documents the new IPC architecture. Updates wikiWorker and startNodeJSWiki to preload workspace ID and load the new plugin. Also updates the plugin build script to compile and copy the new plugin. * test: wiki operation steps * Add per-wiki labeled logging and console hijack Introduces labeled loggers for each wiki, writing logs to separate files. Adds a logFor method to NativeService for logging with labels, updates interfaces, and hijacks worker thread console methods to redirect logs to main process for wiki-specific logging. Refactors workspaceID usage to workspace object for improved context. * Update log handling for wiki worker and tests Enhanced logging tests to check all log files, including wiki logs. Adjusted logger to write wiki worker logs to the main log directory. Updated e2e app script comment for correct usage. * Enable worker thread access to main process services Introduces a proxy system allowing worker threads to call main process services with full type safety and observable support. Adds worker-side service proxy creation, auto-attaches proxies to global.service, and updates service registration to use IPC descriptors. Documentation is added for usage and architecture. * Update ErrorDuringStart.md * chore: upgrade ipc cat and allow clean vite cache * Refactor wiki worker initialization and service readiness Moved wiki worker implementation from wikiWorker.ts to wikiWorker/index.ts and deleted the old file. Added servicesReady.ts to manage worker service readiness and callbacks, and integrated notifyServicesReady into the worker lifecycle. Updated console hijack logic to wait for service readiness before hijacking. Improved worker management in Wiki service to support detaching workers and notifying readiness. * Refactor wiki logging to use centralized logger Removed per-wiki loggers and console hijacking in favor of a single labeled logger. All wiki logs, including errors, are now written to a unified log file. Updated worker and service code to route logs through the main logger and removed obsolete log file naming and management logic. * fix: ipc cat log error * Refactor wiki test paths and improve file save logic Updated test step to use wikiTestRootPath for directory replacements and added wikiTestRootPath to paths.ts for clarity. Improved error handling and directory logic in watch-filesystem-adaptor.ts, including saving tiddlers directly to sub-wiki folders, more informative logging, and ensuring cleanup after file writes is properly awaited. * rename * Initial commit when init a new git. * Initial commit when init a new git. * Initial commit when init a new git. * Initial commit when init a new git. * Initial commit when init a new git. * Initial commit when init a new git. * feat: basic watch-fs * feat: check file not exist * refactor: use exponential-backoff * Initial commit when init a new git. * Initial commit when init a new git. * Initial commit when init a new git. * Initial commit when init a new git. * Initial commit when init a new git. * Initial commit when init a new git. * Initial commit when init a new git. * Initial commit when init a new git. * Initial commit when init a new git. * Initial commit when init a new git. * Initial commit when init a new git. * Initial commit when init a new git. * fix: cleanup * Refactor test setup and cleanup to separate file Moved Before and After hooks from application.ts to a new cleanup.ts file for better organization and separation of concerns. Also removed unused imports and related code from application.ts. Minor type simplification in agent.ts for row parsing. * test: modify and rename * feat: enableFileSystemWatch * refactor: unused utils.ts * Update watch-filesystem-adaptor.ts * refactor: use node-sentinel-file-watcher * refactor: extract to two classes * The logFor method lacks JSDoc describing the level parameter's * Update startNodeJSWiki.ts * fix: napi build * Update electron-rebuild command in workflows Changed the electron-rebuild command in release and test GitHub Actions workflows to use a comma-separated list for native modules instead of multiple -w flags. This simplifies the rebuild step for better-sqlite3 and nsfw modules. * lint * not build nsfw, try use prebuild * Update package.json * Update workerAdapter.ts * remove subWikiPlugin.ts as we use new filesystem adaptor that supports tag based sub wiki * fix: build * fix: wrong type * lint * remove `act(...)` warnings * uninstall chokidar * refactor and test * lint * remove unused logic, as we already use ipc syncadaptor, not afriad of wiki status change * Update translation.json * test: increast timeout in CI * Update application.ts * fix: AI's wrong cleanup logic hidden by as unknown as * fix: AI's wrong as unknown as * Update agent.feature * Update wikiSearchPlugin.ts * fix: A dynamic import callback was not specified.
809 lines
28 KiB
TypeScript
809 lines
28 KiB
TypeScript
/**
|
|
* Wiki Search plugin
|
|
* Handles wiki search tool list injection, tool calling detection and response processing
|
|
*/
|
|
import { WikiChannel } from '@/constants/channels';
|
|
import { matchToolCalling } from '@services/agentDefinition/responsePatternUtility';
|
|
import { container } from '@services/container';
|
|
import { i18n } from '@services/libs/i18n';
|
|
import { t } from '@services/libs/i18n/placeholder';
|
|
import { logger } from '@services/libs/log';
|
|
import serviceIdentifier from '@services/serviceIdentifier';
|
|
import type { IWikiService } from '@services/wiki/interface';
|
|
import type { IWikiEmbeddingService } from '@services/wikiEmbedding/interface';
|
|
import type { IWorkspaceService } from '@services/workspaces/interface';
|
|
import type { ITiddlerFields } from 'tiddlywiki';
|
|
import { z } from 'zod/v4';
|
|
import type { AgentInstanceMessage, IAgentInstanceService } from '../interface';
|
|
import { findPromptById } from '../promptConcat/promptConcat';
|
|
import type { AiAPIConfig } from '../promptConcat/promptConcatSchema';
|
|
import type { IPrompt } from '../promptConcat/promptConcatSchema';
|
|
import { schemaToToolContent } from '../utilities/schemaToToolContent';
|
|
import type { PromptConcatPlugin } from './types';
|
|
|
|
/**
|
|
* Wiki Search Parameter Schema
|
|
* Configuration parameters for the wiki search plugin
|
|
*/
|
|
export const WikiSearchParameterSchema = z.object({
|
|
position: z.enum(['relative', 'absolute', 'before', 'after']).meta({
|
|
title: t('Schema.Position.TypeTitle'),
|
|
description: t('Schema.Position.Type'),
|
|
}),
|
|
targetId: z.string().meta({
|
|
title: t('Schema.Position.TargetIdTitle'),
|
|
description: t('Schema.Position.TargetId'),
|
|
}),
|
|
bottom: z.number().optional().meta({
|
|
title: t('Schema.Position.BottomTitle'),
|
|
description: t('Schema.Position.Bottom'),
|
|
}),
|
|
sourceType: z.enum(['wiki']).meta({
|
|
title: t('Schema.WikiSearch.SourceTypeTitle'),
|
|
description: t('Schema.WikiSearch.SourceType'),
|
|
}),
|
|
toolListPosition: z.object({
|
|
targetId: z.string().meta({
|
|
title: t('Schema.WikiSearch.ToolListPosition.TargetIdTitle'),
|
|
description: t('Schema.WikiSearch.ToolListPosition.TargetId'),
|
|
}),
|
|
position: z.enum(['before', 'after']).meta({
|
|
title: t('Schema.WikiSearch.ToolListPosition.PositionTitle'),
|
|
description: t('Schema.WikiSearch.ToolListPosition.Position'),
|
|
}),
|
|
}).optional().meta({
|
|
title: t('Schema.WikiSearch.ToolListPositionTitle'),
|
|
description: t('Schema.WikiSearch.ToolListPosition'),
|
|
}),
|
|
toolResultDuration: z.number().optional().default(1).meta({
|
|
title: t('Schema.WikiSearch.ToolResultDurationTitle'),
|
|
description: t('Schema.WikiSearch.ToolResultDuration'),
|
|
}),
|
|
}).meta({
|
|
title: t('Schema.WikiSearch.Title'),
|
|
description: t('Schema.WikiSearch.Description'),
|
|
});
|
|
|
|
/**
|
|
* Type definition for wiki search parameters
|
|
*/
|
|
export type WikiSearchParameter = z.infer<typeof WikiSearchParameterSchema>;
|
|
|
|
/**
|
|
* Get the wiki search parameter schema
|
|
* @returns The schema for wiki search parameters
|
|
*/
|
|
export function getWikiSearchParameterSchema() {
|
|
return WikiSearchParameterSchema;
|
|
}
|
|
|
|
/**
|
|
* Parameter schema for Wiki search tool
|
|
*/
|
|
const WikiSearchToolParameterSchema = z.object({
|
|
workspaceName: z.string().meta({
|
|
title: t('Schema.WikiSearch.Tool.Parameters.workspaceName.Title'),
|
|
description: t('Schema.WikiSearch.Tool.Parameters.workspaceName.Description'),
|
|
}),
|
|
searchType: z.enum(['filter', 'vector']).optional().default('filter').meta({
|
|
title: t('Schema.WikiSearch.Tool.Parameters.searchType.Title'),
|
|
description: t('Schema.WikiSearch.Tool.Parameters.searchType.Description'),
|
|
}),
|
|
filter: z.string().optional().meta({
|
|
title: t('Schema.WikiSearch.Tool.Parameters.filter.Title'),
|
|
description: t('Schema.WikiSearch.Tool.Parameters.filter.Description'),
|
|
}),
|
|
query: z.string().optional().meta({
|
|
title: t('Schema.WikiSearch.Tool.Parameters.query.Title'),
|
|
description: t('Schema.WikiSearch.Tool.Parameters.query.Description'),
|
|
}),
|
|
limit: z.number().optional().default(10).meta({
|
|
title: t('Schema.WikiSearch.Tool.Parameters.limit.Title'),
|
|
description: t('Schema.WikiSearch.Tool.Parameters.limit.Description'),
|
|
}),
|
|
threshold: z.number().optional().default(0.7).meta({
|
|
title: t('Schema.WikiSearch.Tool.Parameters.threshold.Title'),
|
|
description: t('Schema.WikiSearch.Tool.Parameters.threshold.Description'),
|
|
}),
|
|
}).meta({
|
|
title: 'wiki-search',
|
|
description: t('Schema.WikiSearch.Tool.Description'),
|
|
examples: [
|
|
{ workspaceName: '我的知识库', searchType: 'filter' as const, filter: '[tag[示例]]', limit: 10, threshold: 0.7 },
|
|
{ workspaceName: '我的知识库', searchType: 'vector' as const, query: '如何使用智能体', limit: 5, threshold: 0.7 },
|
|
],
|
|
});
|
|
|
|
type WikiSearchToolParameter = z.infer<typeof WikiSearchToolParameterSchema>;
|
|
|
|
/**
|
|
* Parameter schema for Wiki update embeddings tool
|
|
*/
|
|
const WikiUpdateEmbeddingsToolParameterSchema = z.object({
|
|
workspaceName: z.string().meta({
|
|
title: t('Schema.WikiSearch.Tool.UpdateEmbeddings.workspaceName.Title'),
|
|
description: t('Schema.WikiSearch.Tool.UpdateEmbeddings.workspaceName.Description'),
|
|
}),
|
|
forceUpdate: z.boolean().optional().default(false).meta({
|
|
title: t('Schema.WikiSearch.Tool.UpdateEmbeddings.forceUpdate.Title'),
|
|
description: t('Schema.WikiSearch.Tool.UpdateEmbeddings.forceUpdate.Description'),
|
|
}),
|
|
})
|
|
.meta({
|
|
title: 'wiki-update-embeddings',
|
|
description: t('Schema.WikiSearch.Tool.UpdateEmbeddings.Description'),
|
|
examples: [
|
|
{ workspaceName: '我的知识库', forceUpdate: false },
|
|
{ workspaceName: 'wiki', forceUpdate: true },
|
|
],
|
|
});
|
|
|
|
type WikiUpdateEmbeddingsToolParameter = z.infer<typeof WikiUpdateEmbeddingsToolParameterSchema>;
|
|
|
|
/**
|
|
* Execute wiki search tool
|
|
*/
|
|
async function executeWikiSearchTool(
|
|
parameters: WikiSearchToolParameter,
|
|
context?: { agentId?: string; messageId?: string; config?: AiAPIConfig },
|
|
): Promise<{ success: boolean; data?: string; error?: string; metadata?: Record<string, unknown> }> {
|
|
try {
|
|
const { workspaceName, searchType = 'filter', filter, query, limit = 10, threshold = 0.7 } = parameters;
|
|
|
|
// Get workspace service
|
|
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
|
|
const wikiService = container.get<IWikiService>(serviceIdentifier.Wiki);
|
|
|
|
// Look up workspace ID from workspace name or ID
|
|
const workspaces = await workspaceService.getWorkspacesAsList();
|
|
const targetWorkspace = workspaces.find(ws => ws.name === workspaceName || ws.id === workspaceName);
|
|
|
|
if (!targetWorkspace) {
|
|
return {
|
|
success: false,
|
|
error: i18n.t('Tool.WikiSearch.Error.WorkspaceNotFound', {
|
|
workspaceName,
|
|
availableWorkspaces: workspaces.map(w => `${w.name} (${w.id})`).join(', '),
|
|
}),
|
|
};
|
|
}
|
|
|
|
const workspaceID = targetWorkspace.id;
|
|
|
|
if (!await workspaceService.exists(workspaceID)) {
|
|
return {
|
|
success: false,
|
|
error: i18n.t('Tool.WikiSearch.Error.WorkspaceNotExist', { workspaceID }),
|
|
};
|
|
}
|
|
|
|
logger.debug('Executing wiki search', {
|
|
workspaceID,
|
|
workspaceName,
|
|
searchType,
|
|
filter,
|
|
query,
|
|
agentId: context?.agentId,
|
|
});
|
|
|
|
// Execute search based on type
|
|
let results: Array<{ title: string; text?: string; fields?: ITiddlerFields; similarity?: number }> = [];
|
|
let searchMetadata: Record<string, unknown> = {
|
|
workspaceID,
|
|
workspaceName,
|
|
searchType,
|
|
};
|
|
|
|
if (searchType === 'vector') {
|
|
// Vector search
|
|
if (!query) {
|
|
return {
|
|
success: false,
|
|
error: i18n.t('Tool.WikiSearch.Error.VectorSearchRequiresQuery'),
|
|
};
|
|
}
|
|
|
|
if (!context?.config) {
|
|
return {
|
|
success: false,
|
|
error: i18n.t('Tool.WikiSearch.Error.VectorSearchRequiresConfig'),
|
|
};
|
|
}
|
|
|
|
const wikiEmbeddingService = container.get<IWikiEmbeddingService>(serviceIdentifier.WikiEmbedding);
|
|
|
|
try {
|
|
const vectorResults = await wikiEmbeddingService.searchSimilar(
|
|
workspaceID,
|
|
query,
|
|
context.config,
|
|
limit,
|
|
threshold,
|
|
);
|
|
|
|
if (vectorResults.length === 0) {
|
|
return {
|
|
success: true,
|
|
data: i18n.t('Tool.WikiSearch.Success.NoVectorResults', { query, workspaceName, threshold }),
|
|
metadata: {
|
|
...searchMetadata,
|
|
query,
|
|
limit,
|
|
threshold,
|
|
resultCount: 0,
|
|
},
|
|
};
|
|
}
|
|
|
|
// Convert vector search results to standard format
|
|
results = vectorResults.map(vr => ({
|
|
title: vr.record.tiddlerTitle,
|
|
text: '', // Vector search returns chunks, full text needs separate retrieval
|
|
similarity: vr.similarity,
|
|
}));
|
|
|
|
// Retrieve full tiddler content for vector results
|
|
const fullContentResults: typeof results = [];
|
|
for (const result of results) {
|
|
try {
|
|
const tiddlerFields = await wikiService.wikiOperationInServer(
|
|
WikiChannel.getTiddlersAsJson,
|
|
workspaceID,
|
|
[result.title],
|
|
);
|
|
if (tiddlerFields.length > 0) {
|
|
fullContentResults.push({
|
|
...result,
|
|
text: tiddlerFields[0].text,
|
|
fields: tiddlerFields[0],
|
|
});
|
|
} else {
|
|
fullContentResults.push(result);
|
|
}
|
|
} catch (error) {
|
|
logger.warn(`Error retrieving full tiddler content for ${result.title}`, {
|
|
error,
|
|
});
|
|
fullContentResults.push(result);
|
|
}
|
|
}
|
|
results = fullContentResults;
|
|
|
|
searchMetadata = {
|
|
...searchMetadata,
|
|
query,
|
|
limit,
|
|
threshold,
|
|
resultCount: results.length,
|
|
};
|
|
} catch (error) {
|
|
logger.error('Vector search failed', {
|
|
error,
|
|
workspaceID,
|
|
query,
|
|
});
|
|
return {
|
|
success: false,
|
|
error: i18n.t('Tool.WikiSearch.Error.VectorSearchFailed', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
}),
|
|
};
|
|
}
|
|
} else {
|
|
// Traditional filter search
|
|
if (!filter) {
|
|
return {
|
|
success: false,
|
|
error: i18n.t('Tool.WikiSearch.Error.FilterSearchRequiresFilter'),
|
|
};
|
|
}
|
|
|
|
const tiddlerTitles = await wikiService.wikiOperationInServer(WikiChannel.runFilter, workspaceID, [filter]);
|
|
|
|
if (tiddlerTitles.length === 0) {
|
|
return {
|
|
success: true,
|
|
data: i18n.t('Tool.WikiSearch.Success.NoResults', { filter, workspaceName }),
|
|
metadata: {
|
|
...searchMetadata,
|
|
filter,
|
|
resultCount: 0,
|
|
},
|
|
};
|
|
}
|
|
|
|
// Retrieve full tiddler content for each tiddler
|
|
for (const title of tiddlerTitles) {
|
|
try {
|
|
const tiddlerFields = await wikiService.wikiOperationInServer(WikiChannel.getTiddlersAsJson, workspaceID, [title]);
|
|
if (tiddlerFields.length > 0) {
|
|
results.push({
|
|
title,
|
|
text: tiddlerFields[0].text,
|
|
fields: tiddlerFields[0],
|
|
});
|
|
} else {
|
|
results.push({ title });
|
|
}
|
|
} catch (error) {
|
|
logger.warn(`Error retrieving tiddler content for ${title}`, {
|
|
error,
|
|
});
|
|
results.push({ title });
|
|
}
|
|
}
|
|
|
|
searchMetadata = {
|
|
...searchMetadata,
|
|
filter,
|
|
resultCount: tiddlerTitles.length,
|
|
returnedCount: results.length,
|
|
};
|
|
}
|
|
|
|
// Format results as text with content
|
|
let content = '';
|
|
if (searchType === 'vector') {
|
|
content = i18n.t('Tool.WikiSearch.Success.VectorCompleted', {
|
|
totalResults: results.length,
|
|
query,
|
|
});
|
|
} else {
|
|
content = i18n.t('Tool.WikiSearch.Success.Completed', {
|
|
totalResults: results.length,
|
|
shownResults: results.length,
|
|
}) + '\n\n';
|
|
}
|
|
|
|
for (const result of results) {
|
|
content += `**Tiddler: ${result.title}**`;
|
|
if (result.similarity !== undefined) {
|
|
content += ` (Similarity: ${(result.similarity * 100).toFixed(1)}%)`;
|
|
}
|
|
content += '\n\n';
|
|
if (result.text) {
|
|
content += '```tiddlywiki\n';
|
|
content += result.text;
|
|
content += '\n```\n\n';
|
|
} else {
|
|
content += '(Content not available)\n\n';
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: content,
|
|
metadata: searchMetadata,
|
|
};
|
|
} catch (error) {
|
|
logger.error('Wiki search tool execution error', {
|
|
error,
|
|
parameters,
|
|
});
|
|
|
|
return {
|
|
success: false,
|
|
error: i18n.t('Tool.WikiSearch.Error.ExecutionFailed', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
}),
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute wiki update embeddings tool
|
|
*/
|
|
async function executeWikiUpdateEmbeddingsTool(
|
|
parameters: WikiUpdateEmbeddingsToolParameter,
|
|
context?: { agentId?: string; messageId?: string; aiConfig?: unknown },
|
|
): Promise<{ success: boolean; data?: string; error?: string; metadata?: Record<string, unknown> }> {
|
|
try {
|
|
const { workspaceName, forceUpdate = false } = parameters;
|
|
|
|
// Get workspace service
|
|
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
|
|
const wikiEmbeddingService = container.get<IWikiEmbeddingService>(serviceIdentifier.WikiEmbedding);
|
|
|
|
// Look up workspace ID from workspace name or ID
|
|
const workspaces = await workspaceService.getWorkspacesAsList();
|
|
const targetWorkspace = workspaces.find(ws => ws.name === workspaceName || ws.id === workspaceName);
|
|
|
|
if (!targetWorkspace) {
|
|
return {
|
|
success: false,
|
|
error: i18n.t('Tool.WikiSearch.UpdateEmbeddings.Error.WorkspaceNotFound', {
|
|
workspaceName,
|
|
availableWorkspaces: workspaces.map(w => `${w.name} (${w.id})`).join(', '),
|
|
}),
|
|
};
|
|
}
|
|
|
|
const workspaceID = targetWorkspace.id;
|
|
|
|
if (!await workspaceService.exists(workspaceID)) {
|
|
return {
|
|
success: false,
|
|
error: i18n.t('Tool.WikiSearch.UpdateEmbeddings.Error.WorkspaceNotExist', { workspaceID }),
|
|
};
|
|
}
|
|
|
|
// Check if AI config is available
|
|
if (!context?.aiConfig) {
|
|
return {
|
|
success: false,
|
|
error: i18n.t('Tool.WikiSearch.UpdateEmbeddings.Error.NoAIConfig'),
|
|
};
|
|
}
|
|
|
|
logger.debug('Executing wiki embedding generation', {
|
|
workspaceID,
|
|
workspaceName,
|
|
forceUpdate,
|
|
agentId: context?.agentId,
|
|
});
|
|
|
|
// Generate embeddings
|
|
await wikiEmbeddingService.generateEmbeddings(
|
|
workspaceID,
|
|
context.aiConfig as Parameters<IWikiEmbeddingService['generateEmbeddings']>[1],
|
|
forceUpdate,
|
|
);
|
|
|
|
// Get stats after generation
|
|
const stats = await wikiEmbeddingService.getEmbeddingStats(workspaceID);
|
|
|
|
const result = i18n.t('Tool.WikiSearch.UpdateEmbeddings.Success.Generated', {
|
|
workspaceName,
|
|
totalEmbeddings: stats.totalEmbeddings,
|
|
totalNotes: stats.totalNotes,
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
data: result,
|
|
metadata: {
|
|
workspaceID,
|
|
workspaceName,
|
|
totalEmbeddings: stats.totalEmbeddings,
|
|
totalNotes: stats.totalNotes,
|
|
forceUpdate,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
logger.error('Wiki update embeddings tool execution error', {
|
|
error,
|
|
parameters,
|
|
});
|
|
|
|
return {
|
|
success: false,
|
|
error: i18n.t('Tool.WikiSearch.UpdateEmbeddings.Error.ExecutionFailed', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
}),
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wiki Search plugin - Prompt processing
|
|
* Handles tool list injection for wiki search and update embeddings functionality
|
|
*/
|
|
export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
|
|
// First tapAsync: Tool list injection
|
|
hooks.processPrompts.tapAsync('wikiSearchPlugin-toolList', async (context, callback) => {
|
|
const { pluginConfig, prompts } = context;
|
|
|
|
if (pluginConfig.pluginId !== 'wikiSearch' || !pluginConfig.wikiSearchParam) {
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
const wikiSearchParameter = pluginConfig.wikiSearchParam;
|
|
|
|
try {
|
|
// Handle tool list injection if toolListPosition is configured
|
|
const toolListPosition = wikiSearchParameter.toolListPosition;
|
|
if (toolListPosition?.targetId) {
|
|
const toolListTarget = findPromptById(prompts, toolListPosition.targetId);
|
|
if (!toolListTarget) {
|
|
logger.warn('Tool list target prompt not found', {
|
|
targetId: toolListPosition.targetId,
|
|
pluginId: pluginConfig.id,
|
|
});
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
// Get available wikis - now handled by workspacesListPlugin
|
|
// The workspaces list will be injected separately by workspacesListPlugin
|
|
|
|
// Inject both wiki-search and wiki-update-embeddings tools
|
|
const wikiSearchToolContent = schemaToToolContent(WikiSearchToolParameterSchema);
|
|
const wikiUpdateEmbeddingsToolContent = schemaToToolContent(WikiUpdateEmbeddingsToolParameterSchema);
|
|
|
|
// Combine both tools into one prompt
|
|
const combinedToolContent = `${wikiSearchToolContent}\n\n${wikiUpdateEmbeddingsToolContent}`;
|
|
|
|
const toolPrompt: IPrompt = {
|
|
id: `wiki-tool-list-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
text: combinedToolContent,
|
|
tags: ['toolList', 'wikiSearch', 'wikiEmbedding'],
|
|
// Use singular caption to match test expectations
|
|
caption: 'Wiki search tool',
|
|
enabled: true,
|
|
};
|
|
|
|
// Insert at specified position
|
|
if (toolListPosition.position === 'before') {
|
|
toolListTarget.parent.splice(toolListTarget.index, 0, toolPrompt);
|
|
} else {
|
|
toolListTarget.parent.splice(toolListTarget.index + 1, 0, toolPrompt);
|
|
}
|
|
|
|
logger.debug('Wiki tool list injected successfully', {
|
|
targetId: toolListPosition.targetId,
|
|
position: toolListPosition.position,
|
|
toolCount: 2, // wiki-search and wiki-update-embeddings
|
|
pluginId: pluginConfig.id,
|
|
});
|
|
}
|
|
|
|
callback();
|
|
} catch (error) {
|
|
logger.error('Error in wiki search tool list injection', {
|
|
error,
|
|
pluginId: pluginConfig.id,
|
|
});
|
|
callback();
|
|
}
|
|
});
|
|
|
|
// 2. Tool execution when AI response is complete
|
|
hooks.responseComplete.tapAsync('wikiSearchPlugin-handler', async (context, callback) => {
|
|
try {
|
|
const { handlerContext, response, handlerConfig } = context;
|
|
|
|
// Find this plugin's configuration from handlerConfig
|
|
const wikiSearchPluginConfig = handlerConfig?.plugins?.find(p => p.pluginId === 'wikiSearch');
|
|
const wikiSearchParameter = wikiSearchPluginConfig?.wikiSearchParam as { toolResultDuration?: number } | undefined;
|
|
const toolResultDuration = wikiSearchParameter?.toolResultDuration || 1; // Default to 1 round
|
|
|
|
if (response.status !== 'done' || !response.content) {
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
// Check for wiki search or update embeddings tool calls in the AI response
|
|
const toolMatch = matchToolCalling(response.content);
|
|
|
|
if (!toolMatch.found || (toolMatch.toolId !== 'wiki-search' && toolMatch.toolId !== 'wiki-update-embeddings')) {
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
logger.debug('Wiki tool call detected', {
|
|
toolId: toolMatch.toolId,
|
|
agentId: handlerContext.agent.id,
|
|
});
|
|
|
|
// Set duration=1 for the AI message containing the tool call
|
|
// Find the most recent AI message (should be the one containing the tool call)
|
|
const aiMessages = handlerContext.agent.messages.filter(message => message.role === 'assistant');
|
|
if (aiMessages.length > 0) {
|
|
const latestAiMessage = aiMessages[aiMessages.length - 1];
|
|
if (latestAiMessage.content === response.content) {
|
|
latestAiMessage.duration = 1;
|
|
latestAiMessage.metadata = {
|
|
...latestAiMessage.metadata,
|
|
containsToolCall: true,
|
|
toolId: toolMatch.toolId,
|
|
};
|
|
|
|
// Notify frontend about the duration change immediately (no debounce delay)
|
|
const agentInstanceService = container.get<IAgentInstanceService>(serviceIdentifier.AgentInstance);
|
|
// Persist the AI message right away so DB ordering reflects this message before tool results
|
|
try {
|
|
if (!latestAiMessage.created) latestAiMessage.created = new Date();
|
|
await agentInstanceService.saveUserMessage(latestAiMessage);
|
|
latestAiMessage.metadata = { ...latestAiMessage.metadata, isPersisted: true };
|
|
} catch (error) {
|
|
logger.warn('Failed to persist AI message containing tool call immediately', {
|
|
error,
|
|
messageId: latestAiMessage.id,
|
|
});
|
|
}
|
|
|
|
// Also update UI immediately
|
|
agentInstanceService.debounceUpdateMessage(latestAiMessage, handlerContext.agent.id, 0); // No delay
|
|
|
|
logger.debug('Set duration=1 for AI tool call message', {
|
|
messageId: latestAiMessage.id,
|
|
toolId: toolMatch.toolId,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Execute the appropriate tool
|
|
try {
|
|
// Check if cancelled before starting tool execution
|
|
if (handlerContext.isCancelled()) {
|
|
logger.debug('Wiki tool cancelled before execution', {
|
|
toolId: toolMatch.toolId,
|
|
agentId: handlerContext.agent.id,
|
|
});
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
// Validate parameters and execute based on tool type
|
|
let result: { success: boolean; data?: string; error?: string; metadata?: Record<string, unknown> };
|
|
let validatedParameters: WikiSearchToolParameter | WikiUpdateEmbeddingsToolParameter;
|
|
|
|
if (toolMatch.toolId === 'wiki-search') {
|
|
validatedParameters = WikiSearchToolParameterSchema.parse(toolMatch.parameters);
|
|
result = await executeWikiSearchTool(
|
|
validatedParameters,
|
|
{
|
|
agentId: handlerContext.agent.id,
|
|
messageId: handlerContext.agent.messages[handlerContext.agent.messages.length - 1]?.id,
|
|
config: handlerContext.agent.aiApiConfig as AiAPIConfig | undefined,
|
|
},
|
|
);
|
|
} else {
|
|
// wiki-update-embeddings
|
|
validatedParameters = WikiUpdateEmbeddingsToolParameterSchema.parse(toolMatch.parameters);
|
|
result = await executeWikiUpdateEmbeddingsTool(
|
|
validatedParameters,
|
|
{
|
|
agentId: handlerContext.agent.id,
|
|
messageId: handlerContext.agent.messages[handlerContext.agent.messages.length - 1]?.id,
|
|
aiConfig: handlerContext.agent.aiApiConfig,
|
|
},
|
|
);
|
|
}
|
|
|
|
// Check if cancelled after tool execution
|
|
if (handlerContext.isCancelled()) {
|
|
logger.debug('Wiki tool cancelled after execution', {
|
|
toolId: toolMatch.toolId,
|
|
agentId: handlerContext.agent.id,
|
|
});
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
// Format the tool result for display
|
|
let toolResultText: string;
|
|
let isError = false;
|
|
|
|
if (result.success && result.data) {
|
|
toolResultText = `<functions_result>\nTool: ${toolMatch.toolId}\nParameters: ${JSON.stringify(validatedParameters)}\nResult: ${result.data}\n</functions_result>`;
|
|
} else {
|
|
isError = true;
|
|
toolResultText = `<functions_result>\nTool: ${toolMatch.toolId}\nParameters: ${JSON.stringify(validatedParameters)}\nError: ${result.error}\n</functions_result>`;
|
|
}
|
|
|
|
// Set up actions to continue the conversation with tool results
|
|
const responseContext = context;
|
|
if (!responseContext.actions) {
|
|
responseContext.actions = {};
|
|
}
|
|
responseContext.actions.yieldNextRoundTo = 'self';
|
|
|
|
logger.debug('Wiki search setting yieldNextRoundTo=self', {
|
|
toolId: 'wiki-search',
|
|
agentId: handlerContext.agent.id,
|
|
messageCount: handlerContext.agent.messages.length,
|
|
toolResultPreview: toolResultText.slice(0, 200),
|
|
});
|
|
|
|
// Immediately add the tool result message to history BEFORE calling toolExecuted
|
|
const nowTool = new Date();
|
|
const toolResultMessage: AgentInstanceMessage = {
|
|
id: `tool-result-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
agentId: handlerContext.agent.id,
|
|
role: 'tool', // Tool result message
|
|
content: toolResultText,
|
|
created: nowTool,
|
|
modified: nowTool,
|
|
duration: toolResultDuration, // Use configurable duration - default 1 round for tool results
|
|
metadata: {
|
|
isToolResult: true,
|
|
isError,
|
|
toolId: 'wiki-search',
|
|
toolParameters: validatedParameters,
|
|
isPersisted: false, // Required by messageManagementPlugin to identify new tool results
|
|
isComplete: true, // Mark as complete to prevent messageManagementPlugin from overwriting content
|
|
artificialOrder: Date.now() + 10, // Additional ordering hint
|
|
},
|
|
};
|
|
handlerContext.agent.messages.push(toolResultMessage);
|
|
|
|
// Do not persist immediately here. Let messageManagementPlugin handle persistence
|
|
|
|
// Signal that tool was executed AFTER adding and persisting the message
|
|
await hooks.toolExecuted.promise({
|
|
handlerContext,
|
|
toolResult: {
|
|
success: true,
|
|
data: result.success ? result.data : result.error,
|
|
metadata: { toolCount: 1 },
|
|
},
|
|
toolInfo: {
|
|
toolId: 'wiki-search',
|
|
parameters: validatedParameters,
|
|
originalText: toolMatch.originalText,
|
|
},
|
|
requestId: context.requestId,
|
|
});
|
|
|
|
logger.debug('Wiki search tool execution completed', {
|
|
toolResultText,
|
|
actions: responseContext.actions,
|
|
toolResultMessageId: toolResultMessage.id,
|
|
aiMessageDuration: aiMessages[aiMessages.length - 1]?.duration,
|
|
});
|
|
} catch (error) {
|
|
logger.error('Wiki search tool execution failed', {
|
|
error,
|
|
toolCall: toolMatch,
|
|
});
|
|
|
|
// Set up error response for next round
|
|
const responseContext = context;
|
|
if (!responseContext.actions) {
|
|
responseContext.actions = {};
|
|
}
|
|
responseContext.actions.yieldNextRoundTo = 'self';
|
|
const errorMessage = `<functions_result>
|
|
Tool: wiki-search
|
|
Error: ${error instanceof Error ? error.message : String(error)}
|
|
</functions_result>`;
|
|
|
|
// Add error message to history BEFORE calling toolExecuted
|
|
// Use the current time; order will be determined by save order
|
|
const nowError = new Date();
|
|
const errorResultMessage: AgentInstanceMessage = {
|
|
id: `tool-error-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
agentId: handlerContext.agent.id,
|
|
role: 'tool', // Tool error message
|
|
content: errorMessage,
|
|
created: nowError,
|
|
modified: nowError,
|
|
duration: 2, // Error messages are visible to AI for 2 rounds: immediate + next round to allow explanation
|
|
metadata: {
|
|
isToolResult: true,
|
|
isError: true,
|
|
toolId: 'wiki-search',
|
|
isPersisted: false, // Required by messageManagementPlugin to identify new tool results
|
|
isComplete: true, // Mark as complete to prevent messageManagementPlugin from overwriting content
|
|
},
|
|
};
|
|
handlerContext.agent.messages.push(errorResultMessage);
|
|
|
|
// Do not persist immediately; let messageManagementPlugin handle it during toolExecuted
|
|
await hooks.toolExecuted.promise({
|
|
handlerContext,
|
|
toolResult: {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
},
|
|
toolInfo: {
|
|
toolId: 'wiki-search',
|
|
parameters: {},
|
|
},
|
|
});
|
|
|
|
logger.debug('Wiki search tool execution failed but error result added', {
|
|
errorResultMessageId: errorResultMessage.id,
|
|
aiMessageDuration: aiMessages[aiMessages.length - 1]?.duration,
|
|
});
|
|
}
|
|
|
|
callback();
|
|
} catch (error) {
|
|
logger.error('Error in wiki search handler plugin', { error });
|
|
callback();
|
|
}
|
|
});
|
|
};
|