From f3a0252ab6c6b2d712b8ff4f65a2639626dcf213 Mon Sep 17 00:00:00 2001 From: duanfuxiang Date: Sun, 29 Jun 2025 08:28:50 +0800 Subject: [PATCH] fix trans tool --- src/components/chat-view/ChatView.tsx | 61 ++-- .../MarkdownTransformationToolBlock.tsx | 44 +-- src/components/chat-view/ReactMarkdown.tsx | 55 +-- src/core/prompts/sections/capabilities.ts | 42 ++- src/core/prompts/sections/objective.ts | 26 +- src/core/prompts/sections/rules.ts | 33 ++ src/core/prompts/sections/system-info.ts | 1 - .../prompts/sections/tool-use-guidelines.ts | 42 +++ src/core/prompts/system.ts | 7 +- src/core/prompts/tools/call-insights.ts | 26 ++ src/core/prompts/tools/dataview-query.ts | 9 +- src/core/prompts/tools/index.ts | 25 +- src/core/prompts/tools/tool-groups.ts | 13 +- src/database/database-manager.ts | 7 + src/database/modules/insight/index.ts | 2 + .../modules/insight/insight-manager.ts | 321 ++++++++++++++++++ .../modules/insight/insight-repository.ts | 274 +++++++++++++++ .../modules/vector/vector-repository.ts | 2 +- src/database/schema.ts | 60 ++++ src/database/sql.ts | 113 ++++++ src/types/apply.ts | 40 +-- src/utils/modes.ts | 6 +- src/utils/parse-infio-block.ts | 187 +--------- src/utils/prompt-generator.ts | 214 ++++++++---- src/utils/tool-groups.ts | 4 +- 25 files changed, 1173 insertions(+), 441 deletions(-) create mode 100644 src/core/prompts/tools/call-insights.ts create mode 100644 src/database/modules/insight/index.ts create mode 100644 src/database/modules/insight/insight-manager.ts create mode 100644 src/database/modules/insight/insight-repository.ts diff --git a/src/components/chat-view/ChatView.tsx b/src/components/chat-view/ChatView.tsx index 8002b60..da3ffb7 100644 --- a/src/components/chat-view/ChatView.tsx +++ b/src/components/chat-view/ChatView.tsx @@ -829,41 +829,25 @@ const Chat = forwardRef((props, ref) => { mentionables: [], } } - } else if (toolArgs.type === 'analyze_paper' || - toolArgs.type === 'key_insights' || - toolArgs.type === 'dense_summary' || - toolArgs.type === 'reflections' || - toolArgs.type === 'table_of_contents' || - toolArgs.type === 'simple_summary') { - // 处理文档转换工具 - console.log('toolArgs', toolArgs) - + } else if (toolArgs.type === 'call_transformations') { + // Handling for the unified transformations tool try { - // 获取文件 - const targetFile = app.vault.getFileByPath(toolArgs.path) + const targetFile = app.vault.getFileByPath(toolArgs.path); if (!targetFile) { - throw new Error(`文件未找到: ${toolArgs.path}`) + throw new Error(`File not found: ${toolArgs.path}`); } - // 读取文件内容 - const fileContent = await readTFileContentPdf(targetFile, app.vault, app) - - // 映射工具类型到转换类型 - const transformationTypeMap: Record = { - 'analyze_paper': TransformationType.ANALYZE_PAPER, - 'key_insights': TransformationType.KEY_INSIGHTS, - 'dense_summary': TransformationType.DENSE_SUMMARY, - 'reflections': TransformationType.REFLECTIONS, - 'table_of_contents': TransformationType.TABLE_OF_CONTENTS, - 'simple_summary': TransformationType.SIMPLE_SUMMARY + const fileContent = await readTFileContentPdf(targetFile, app.vault, app); + + // The transformation type is now passed directly in the arguments + const transformationType = toolArgs.transformation as TransformationType; + + // Validate that the transformation type is a valid enum member + if (!Object.values(TransformationType).includes(transformationType)) { + throw new Error(`Unsupported transformation type: ${transformationType}`); } - const transformationType = transformationTypeMap[toolArgs.type] - if (!transformationType) { - throw new Error(`不支持的转换类型: ${toolArgs.type}`) - } - - // 执行转换 + // Execute the transformation const transformationResult = await runTransformation({ content: fileContent, transformationType, @@ -872,18 +856,17 @@ const Chat = forwardRef((props, ref) => { provider: settings.applyModelProvider, modelId: settings.applyModelId, } - }) + }); if (!transformationResult.success) { - throw new Error(transformationResult.error || '转换失败') + throw new Error(transformationResult.error || 'Transformation failed'); } - // 构建结果消息 - let formattedContent = `[${toolArgs.type}] 转换完成:\n\n${transformationResult.result}` + // Build the result message + let formattedContent = `[${transformationType}] transformation complete:\n\n${transformationResult.result}`; - // 如果内容被截断,添加提示 if (transformationResult.truncated) { - formattedContent += `\n\n*注意: 原始内容过长(${transformationResult.originalTokens} tokens),已截断为${transformationResult.processedTokens} tokens进行处理*` + formattedContent += `\n\n*Note: The original content was too long (${transformationResult.originalTokens} tokens) and was truncated to ${transformationResult.processedTokens} tokens for processing.*`; } return { @@ -898,9 +881,9 @@ const Chat = forwardRef((props, ref) => { id: uuidv4(), mentionables: [], } - } + }; } catch (error) { - console.error(`转换失败 (${toolArgs.type}):`, error) + console.error(`Transformation failed (${toolArgs.transformation}):`, error); return { type: toolArgs.type, applyMsgId, @@ -909,11 +892,11 @@ const Chat = forwardRef((props, ref) => { role: 'user', applyStatus: ApplyStatus.Idle, content: null, - promptContent: `[${toolArgs.type}] 转换失败: ${error instanceof Error ? error.message : String(error)}`, + promptContent: `[${toolArgs.transformation}] transformation failed: ${error instanceof Error ? error.message : String(error)}`, id: uuidv4(), mentionables: [], } - } + }; } } } catch (error) { diff --git a/src/components/chat-view/Markdown/MarkdownTransformationToolBlock.tsx b/src/components/chat-view/Markdown/MarkdownTransformationToolBlock.tsx index e81c8de..8e62d10 100644 --- a/src/components/chat-view/Markdown/MarkdownTransformationToolBlock.tsx +++ b/src/components/chat-view/Markdown/MarkdownTransformationToolBlock.tsx @@ -5,21 +5,19 @@ import { useApp } from "../../../contexts/AppContext" import { ApplyStatus, ToolArgs } from "../../../types/apply" import { openMarkdownFile } from "../../../utils/obsidian" -export type TransformationToolType = 'analyze_paper' | 'key_insights' | 'dense_summary' | 'reflections' | 'table_of_contents' | 'simple_summary' +export type TransformationToolType = 'call_transformations' interface MarkdownTransformationToolBlockProps { applyStatus: ApplyStatus onApply: (args: ToolArgs) => void toolType: TransformationToolType path: string - depth?: number - format?: string - include_summary?: boolean + transformation?: string finish: boolean } -const getToolConfig = (toolType: TransformationToolType) => { - switch (toolType) { +const getTransformationConfig = (transformation: string) => { + switch (transformation) { case 'analyze_paper': return { icon: , @@ -68,15 +66,12 @@ const getToolConfig = (toolType: TransformationToolType) => { export default function MarkdownTransformationToolBlock({ applyStatus, onApply, - toolType, path, - depth, - format, - include_summary, + transformation, finish }: MarkdownTransformationToolBlockProps) { const app = useApp() - const config = getToolConfig(toolType) + const config = getTransformationConfig(transformation || '') const handleClick = () => { if (path) { @@ -86,32 +81,15 @@ export default function MarkdownTransformationToolBlock({ React.useEffect(() => { if (finish && applyStatus === ApplyStatus.Idle) { - // 构建符合标准ToolArgs类型的参数 - if (toolType === 'table_of_contents') { - onApply({ - type: toolType, - path: path || '', - depth, - format, - include_summary - }) - } else { - onApply({ - type: toolType, - path: path || '', - }) - } + onApply({ + type: 'call_transformations', + path: path || '', + transformation: transformation || '' + }) } }, [finish]) const getDisplayText = () => { - if (toolType === 'table_of_contents') { - let text = `${config.title}: ${path || '未指定路径'}` - if (depth) text += ` (深度: ${depth})` - if (format) text += ` (格式: ${format})` - if (include_summary) text += ` (包含摘要)` - return text - } return `${config.title}: ${path || '未指定路径'}` } diff --git a/src/components/chat-view/ReactMarkdown.tsx b/src/components/chat-view/ReactMarkdown.tsx index 28507b6..994f8c3 100644 --- a/src/components/chat-view/ReactMarkdown.tsx +++ b/src/components/chat-view/ReactMarkdown.tsx @@ -213,61 +213,14 @@ function ReactMarkdown({ outputFormat={block.outputFormat} finish={block.finish} /> - ) : block.type === 'analyze_paper' ? ( + ) : block.type === 'call_transformations' ? ( - ) : block.type === 'key_insights' ? ( - - ) : block.type === 'dense_summary' ? ( - - ) : block.type === 'reflections' ? ( - - ) : block.type === 'table_of_contents' ? ( - - ) : block.type === 'simple_summary' ? ( - ) : block.type === 'tool_result' ? ( diff --git a/src/core/prompts/sections/capabilities.ts b/src/core/prompts/sections/capabilities.ts index 4328721..228a48c 100644 --- a/src/core/prompts/sections/capabilities.ts +++ b/src/core/prompts/sections/capabilities.ts @@ -4,6 +4,39 @@ const RegexSearchFilesInstructions = "\n- You can use regex_search_files to perf const SemanticSearchFilesInstructions = "\n- You can use semantic_search_files to find content based on meaning rather than exact text matches. Semantic search uses embedding vectors to understand concepts and ideas, finding relevant content even when keywords differ. This is especially powerful for discovering thematically related notes, answering conceptual questions about your knowledge base, or finding content when you don't know the exact wording used in the notes." +function getAskModeCapabilitiesSection( + cwd: string, + searchFilesTool: string, +): string { + let searchFilesInstructions: string; + switch (searchFilesTool) { + case 'match': + searchFilesInstructions = MatchSearchFilesInstructions; + break; + case 'regex': + searchFilesInstructions = RegexSearchFilesInstructions; + break; + case 'semantic': + searchFilesInstructions = SemanticSearchFilesInstructions; + break; + default: + searchFilesInstructions = ""; + } + return `==== + +CAPABILITIES + +Your primary role is to act as an intelligent Knowledge Assistant deeply integrated within this Obsidian vault. You are equipped with four core capabilities that map directly to user intents: + +1. **Insight & Understanding**: This is your most powerful capability. You can synthesize, analyze, compare, and understand content across various scopes. By using the \`insights\` tool, you can process single notes, entire folders, or notes with specific tags to extract high-level insights, summaries, and key points. This allows you to answer complex questions without needing to manually read every single file. + +2. **Lookup & Navigate**: You can efficiently locate specific information. You can perform semantic searches for concepts (\`search_files\`) and structured queries for metadata like tags or dates (\`dataview_query\`). The initial file list in \`environment_details\` provides a starting point, but you should use your search tools to find the most relevant information. + +3. **Create & Generate**: You can act as a writing partner to create new content. Using the \`write_to_file\` tool, you can draft new notes, brainstorm ideas, or generate structured documents from templates, helping the user expand their knowledge base. + +4. **Action & Integration**: You can connect the knowledge in this vault to the outside world. Through the \`use_mcp_tool\`, you can interact with external services like task managers or calendars, turning insights into actions.${searchFilesInstructions}` +} + function getObsidianCapabilitiesSection( cwd: string, searchFilesTool: string, @@ -78,13 +111,16 @@ CAPABILITIES export function getCapabilitiesSection( mode: string, cwd: string, - searchWebTool: string, + searchFileTool: string, ): string { + if (mode === 'ask') { + return getAskModeCapabilitiesSection(cwd, searchFileTool); + } if (mode === 'research') { return getDeepResearchCapabilitiesSection(); } if (mode === 'learn') { - return getLearnModeCapabilitiesSection(cwd, searchWebTool); + return getLearnModeCapabilitiesSection(cwd, searchFileTool); } - return getObsidianCapabilitiesSection(cwd, searchWebTool); + return getObsidianCapabilitiesSection(cwd, searchFileTool); } diff --git a/src/core/prompts/sections/objective.ts b/src/core/prompts/sections/objective.ts index 912b1a5..a13ac9a 100644 --- a/src/core/prompts/sections/objective.ts +++ b/src/core/prompts/sections/objective.ts @@ -1,3 +1,24 @@ +function getAskModeObjectiveSection(): string { + return `==== + +OBJECTIVE + +Your primary objective is to accurately fulfill the user's request by methodically following a clear, iterative process. + +1. **Analyze and Classify Intent**: First, analyze the user's request to determine its core intent based on the four categories you know: **Insight & Understanding**, **Lookup & Navigate**, **Create & Generate**, or **Action & Integration**. This classification is the most critical step and dictates your entire plan. + +2. **Formulate a Plan in **: Inside \`\` tags, state the identified intent and your step-by-step plan. Your plan must start with selecting the single most appropriate primary tool for the intent (e.g., \`insights\` for understanding, \`search_files\` for lookup, \`write_to_file\` for creation). + +3. **Execute Tools with Precision**: Before invoking a tool, you must verify that you have all its required parameters. + * Carefully consider the user's request and the conversation context to see if values for required parameters can be reasonably inferred. + * If all required parameters are present or can be inferred, proceed with the tool use. + * However, if a required parameter is missing and cannot be inferred, **DO NOT** invoke the tool. Instead, use the \`ask_followup_question\` tool to ask the user for the specific missing information. Do not ask for optional parameters if they are not provided. + +4. **Synthesize and Respond Directly**: After receiving the tool result, construct your final answer. Do not end your responses with generic questions like "Is there anything else I can help with?". When you have completed the task, state that you are done. + +5. **Adhere to All Rules**: In every step, you must strictly adhere to all constraints and formatting requirements defined in the RULES section, especially regarding source citation and tool selection guidelines. +` +} function getLearnModeObjectiveSection(): string { return `==== @@ -20,7 +41,7 @@ You enhance learning and comprehension by transforming information into digestib Before using any tool, analyze the learning context within tags. Consider the user's learning goals, existing knowledge level, and how the current task fits into their broader learning objectives. Prioritize transformation tools for content analysis and focus on creating materials that promote active learning rather than passive consumption.` } -function getDeepResearchObjectiveSection(): string { +function getDeepResearchObjectiveSection(): string { return `==== OBJECTIVE @@ -51,6 +72,9 @@ You accomplish a given task iteratively, breaking it down into clear steps and w } export function getObjectiveSection(mode: string): string { + if (mode === 'ask') { + return getAskModeObjectiveSection(); + } if (mode === 'research') { return getDeepResearchObjectiveSection(); } diff --git a/src/core/prompts/sections/rules.ts b/src/core/prompts/sections/rules.ts index 0223e87..b23ec43 100644 --- a/src/core/prompts/sections/rules.ts +++ b/src/core/prompts/sections/rules.ts @@ -69,6 +69,36 @@ RULES ` } +function getAskModeRulesSection( + cwd: string, + searchTool: string, +): string { + return `==== + +RULES + +- Your current obsidian directory is: ${cwd.toPosix()} +${getSearchInstructions(searchTool)} +- **Mandatory Thinking Process**: You MUST use tags to outline your reasoning and plan before every action. This is not optional. +- **Intent-Driven Tool Selection**: You must strictly follow the "Intent Analysis" guide to select the single most appropriate primary tool (\`search_files\`, \`dataview_query\`, \`insights\`, \`write_to_file\`, or \`use_mcp_tool\`). +- **Use 'insights' for Understanding**: For any request that involves summarizing, analyzing, comparing, or understanding content, your primary tool MUST be \`insights\`. Do not try to manually read multiple files and synthesize them yourself unless the \`insights\` tool is insufficient. +- **Cite Sources with [[WikiLinks]]**: You MUST use Obsidian-style [[WikiLinks]] to reference all source notes. This is a critical rule. The link must be the full relative path of the note from the vault root (e.g., \`[[Daily Notes/2024-05-21]]\`). Never use bare filenames or standard Markdown links (\`[text](path)\`) when referring to notes within the vault. +- **One Tool at a Time**: Use only one tool per message. Wait for the result before deciding on the next step. + +## Thinking Tag Structure +You are required to use the following structure inside your tags: + + +**1. Intent:** [Your analysis of the user's intent: Lookup & Navigate, Insight & Understanding, Create & Generate, or Action & Integration] +**2. Plan:** + - Step 1: Based on the intent, I will use the \`[Primary Tool]\` to \`[Action for this tool, e.g., 'get insights on Topic X from the whole vault']\`. + - Step 2: (If necessary, based on the result of Step 1) Use a follow-up tool to refine or act on the result. + - Step 3: Construct the final answer, citing all sources with [[WikiLinks]] if applicable. +**3. Justification:** [Briefly explain why you chose this primary tool based on your intent analysis.] + +` +} + function getObsidianRulesSection( mode: string, cwd: string, @@ -107,6 +137,9 @@ export function getRulesSection( diffStrategy?: DiffStrategy, experiments?: Record | undefined, ): string { + if (mode === 'ask') { + return getAskModeRulesSection(cwd, searchTool); + } if (mode === 'research') { return getDeepResearchRulesSection(); } diff --git a/src/core/prompts/sections/system-info.ts b/src/core/prompts/sections/system-info.ts index fe199de..8c7033a 100644 --- a/src/core/prompts/sections/system-info.ts +++ b/src/core/prompts/sections/system-info.ts @@ -1,4 +1,3 @@ -import os from "os" import { Platform } from 'obsidian'; diff --git a/src/core/prompts/sections/tool-use-guidelines.ts b/src/core/prompts/sections/tool-use-guidelines.ts index fab1be5..6a746de 100644 --- a/src/core/prompts/sections/tool-use-guidelines.ts +++ b/src/core/prompts/sections/tool-use-guidelines.ts @@ -1,3 +1,42 @@ +function getAskModeToolUseGuidelines(): string { + return `# Workflow & Decision Guide + +When you receive a user question, follow this systematic thinking process to build your action plan: + +## Step 1: Intent Analysis +This is your most important task. Carefully analyze the user's question to determine its primary intent from the following categories: + +* **Lookup & Navigate**: The user wants to *find and locate* raw information or notes within their vault. The goal is to get a pointer to the original content. + * *Keywords*: "find...", "search for...", "list all notes...", "open the note about...", "where did I mention..." + * *Primary Tools*: \`search_files\`, \`dataview_query\`. + +* **Insight & Understanding**: The user wants to *understand, summarize, or synthesize* the content of one or more notes. The goal is a processed answer, not the raw text. This is the primary purpose of the \`insights\` tool. + * *Keywords*: "summarize...", "what are the key points of...", "explain my thoughts on...", "compare A and B...", "analyze the folder..." + * *Primary Tool*: \`insights\`. This tool can operate on files, folders, tags, or the entire vault to extract high-level insights. + +* **Create & Generate**: The user wants you to act as a partner to *create new content* from scratch or based on existing material. The goal is a new note in their vault. + * *Keywords*: "draft a blog post...", "create a new note for...", "brainstorm ideas about...", "generate a plan for..." + * *Primary Tool*: \`write_to_file\`. + +* **Action & Integration**: The user's request requires interaction with a service *outside* of Obsidian, such as a task manager or calendar. + * *Keywords*: "create a task...", "send an email to...", "schedule an event..." + * *Primary Tool*: \`use_mcp_tool\`. + +## Step 2: Primary Tool Execution +Based on your intent analysis, select and execute the single most appropriate primary tool to get initial information. + +## Step 3: Enhancement & Follow-up (If Needed) +After getting the primary tool result, decide if you need follow-up tools to complete the answer: + +- If \`search_files\` or \`dataview_query\` returned a list of notes and you need to understand their content → Use the \`insights\` tool on the relevant files or folders to extract key information. +- If you need to examine specific raw content → Use \`read_file\` to get the full text of particular notes. +- If you need to save your findings → Use \`write_to_file\` to create a new, well-structured summary note. + +## Step 4: Answer Construction & Citation +Build your final response based on all collected and processed information. When the answer is based on vault content, you **MUST** use \`[[WikiLinks]]\` to cite all source notes you consulted. +` +} + function getLearnModeToolUseGuidelines(): string { return `# Tool Use Guidelines @@ -68,6 +107,9 @@ By waiting for and carefully considering the user's response after each tool use } export function getToolUseGuidelinesSection(mode?: string): string { + if (mode === 'ask') { + return getAskModeToolUseGuidelines() + } if (mode === 'learn') { return getLearnModeToolUseGuidelines() } diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 77be92a..f974401 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -1,4 +1,3 @@ - import * as path from 'path' import { App, normalizePath } from 'obsidian' @@ -10,9 +9,9 @@ import { ModeConfig, PromptComponent, defaultModeSlug, + defaultModes, getGroupName, - getModeBySlug, - defaultModes + getModeBySlug } from "../../utils/modes" import { DiffStrategy } from "../diff/DiffStrategy" import { McpHub } from "../mcp/McpHub" @@ -138,7 +137,7 @@ ${getRulesSection( experiments, )} -${getSystemInfoSection(cwd)} +// ${getSystemInfoSection(cwd)} ${getObjectiveSection(mode)} diff --git a/src/core/prompts/tools/call-insights.ts b/src/core/prompts/tools/call-insights.ts new file mode 100644 index 0000000..1c6138d --- /dev/null +++ b/src/core/prompts/tools/call-insights.ts @@ -0,0 +1,26 @@ +import { ToolArgs } from "./types" + +export function getCallInsightsDescription(args: ToolArgs): string { + return `## insights +Description: Use for **Information Processing**. After reading a note's content, use this tool to process and distill the information in various ways. You must choose the most appropriate transformation type based on your goal. +Parameters: +- path: (required) The path to the file or folder to be processed (relative to the current working directory: ${args.cwd}). +- transformation: (required) The type of transformation to apply. Must be one of the following: + - **simple_summary**: Creates a clear, simple summary. Use when you need to quickly understand the main points or explain a complex topic easily. + - **key_insights**: Extracts high-level, critical insights and non-obvious connections. Use when you want to understand the deeper meaning or strategic implications. + - **dense_summary**: Provides a comprehensive, information-rich summary. Use when detail is important but you need it in a condensed format. + - **reflections**: Generates deep, reflective questions and perspectives to s·park new ideas. Use when you want to think critically *with* your notes. + - **table_of_contents**: Creates a navigable table of contents for a long document or folder. Use for structuring and organizing content. + - **analyze_paper**: Performs an in-depth analysis of an academic paper, breaking down its components. Use for scholarly or research documents. +Usage: + +path/to/your/file.md +simple_summary + + +Example: Getting the key insights from a project note + +Projects/Project_Alpha_Retrospective.md +key_insights +` +} diff --git a/src/core/prompts/tools/dataview-query.ts b/src/core/prompts/tools/dataview-query.ts index 593c547..95a9517 100644 --- a/src/core/prompts/tools/dataview-query.ts +++ b/src/core/prompts/tools/dataview-query.ts @@ -2,12 +2,14 @@ import { ToolArgs } from "./types" export function getDataviewQueryDescription(args: ToolArgs): string { return `## dataview_query -Description: Execute advanced queries using the Dataview plugin to retrieve and filter note information across multiple dimensions. Supports complex queries by time, tags, task status, file properties, and more. This is a powerful tool for obtaining structured note data, particularly useful for statistical analysis, content organization, and progress tracking scenarios. +Description: Use for **Metadata Lookup**. Executes a Dataview query to find notes based on structural attributes like tags, folders, dates, or other metadata properties. This is your primary tool when the user's request is about filtering or finding notes with specific characteristics, not about understanding a concept. Parameters: -- query: (required) The Dataview query statement to execute. Supports DQL (Dataview Query Language) syntax, including TABLE, LIST, TASK query types -- output_format: (optional) Output format, options: table, list, task, calendar (defaults to table) +- query: (required) The Dataview query statement (DQL). Common Query Patterns: +- Find notes with a tag: \`LIST FROM #project\` +- Find notes in a folder: \`LIST FROM "Meetings"\` +- Find notes by task completion: \`TASK WHERE completed\` **Time-based Queries:** - Recently created: \`WHERE file.ctime >= date(today) - dur(7 days)\` @@ -21,7 +23,6 @@ Common Query Patterns: **Task-based Queries:** - Incomplete tasks: \`TASK WHERE !completed\` -- Completed tasks: \`TASK WHERE completed\` - Specific priority tasks: \`TASK WHERE contains(text, "high priority")\` **File Property Queries:** diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts index 465e496..5443067 100644 --- a/src/core/prompts/tools/index.ts +++ b/src/core/prompts/tools/index.ts @@ -6,6 +6,7 @@ import { McpHub } from "../../mcp/McpHub" import { getAccessMcpResourceDescription } from "./access-mcp-resource" import { getAskFollowupQuestionDescription } from "./ask-followup-question" import { getAttemptCompletionDescription } from "./attempt-completion" +import { getCallInsightsDescription } from "./call-insights" import { getDataviewQueryDescription } from "./dataview-query" import { getFetchUrlsContentDescription } from "./fetch-url-content" import { getInsertContentDescription } from "./insert-content" @@ -18,14 +19,6 @@ import { getSwitchModeDescription } from "./switch-mode" import { ALWAYS_AVAILABLE_TOOLS, TOOL_GROUPS } from "./tool-groups" import { ToolArgs } from "./types" import { getUseMcpToolDescription } from "./use-mcp-tool" -import { - getAnalyzePaperDescription, - getDenseSummaryDescription, - getKeyInsightsDescription, - getReflectionsDescription, - getSimpleSummaryDescription, - getTableOfContentsDescription -} from "./use-transformations-tool" import { getWriteToFileDescription } from "./write-to-file" // Map of tool names to their description functions @@ -34,6 +27,7 @@ const toolDescriptionMap: Record string | undefined> write_to_file: (args) => getWriteToFileDescription(args), search_files: (args) => getSearchFilesDescription(args), list_files: (args) => getListFilesDescription(args), + insights: (args) => getCallInsightsDescription(args), dataview_query: (args) => getDataviewQueryDescription(args), ask_followup_question: () => getAskFollowupQuestionDescription(), attempt_completion: () => getAttemptCompletionDescription(), @@ -46,12 +40,6 @@ const toolDescriptionMap: Record string | undefined> args.diffStrategy ? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions }) : "", search_web: (args): string | undefined => getSearchWebDescription(args), fetch_urls_content: (args): string | undefined => getFetchUrlsContentDescription(args), - analyze_paper: (args) => getAnalyzePaperDescription(args), - key_insights: (args) => getKeyInsightsDescription(args), - dense_summary: (args) => getDenseSummaryDescription(args), - reflections: (args) => getReflectionsDescription(args), - table_of_contents: (args) => getTableOfContentsDescription(args), - simple_summary: (args) => getSimpleSummaryDescription(args), } export function getToolDescriptionsForMode( @@ -117,11 +105,8 @@ export function getToolDescriptionsForMode( // Export individual description functions for backward compatibility export { - getAccessMcpResourceDescription, getAnalyzePaperDescription, getAskFollowupQuestionDescription, - getAttemptCompletionDescription, - getDataviewQueryDescription, getDenseSummaryDescription, getInsertContentDescription, getKeyInsightsDescription, getListFilesDescription, - getReadFileDescription, getReflectionsDescription, getSearchAndReplaceDescription, - getSearchFilesDescription, getSimpleSummaryDescription, getSwitchModeDescription, getTableOfContentsDescription, getUseMcpToolDescription, - getWriteToFileDescription + getAccessMcpResourceDescription, getReadFileDescription, getWriteToFileDescription, getSearchFilesDescription, getListFilesDescription, + getDataviewQueryDescription, getAskFollowupQuestionDescription, getAttemptCompletionDescription, getSwitchModeDescription, getInsertContentDescription, + getUseMcpToolDescription, getSearchAndReplaceDescription, getSearchWebDescription, getFetchUrlsContentDescription, getCallInsightsDescription as getCallInsightsDescription } diff --git a/src/core/prompts/tools/tool-groups.ts b/src/core/prompts/tools/tool-groups.ts index 121be69..378bd6b 100644 --- a/src/core/prompts/tools/tool-groups.ts +++ b/src/core/prompts/tools/tool-groups.ts @@ -15,15 +15,10 @@ export const TOOL_DISPLAY_NAMES = { dataview_query: "query dataview", use_mcp_tool: "use mcp tools", access_mcp_resource: "access mcp resources", + insights: "call insights", ask_followup_question: "ask questions", attempt_completion: "complete tasks", switch_mode: "switch modes", - analyze_paper: "analyze papers", - key_insights: "extract key insights", - dense_summary: "create dense summaries", - reflections: "generate reflections", - table_of_contents: "create table of contents", - simple_summary: "create simple summaries", } as const // Define available tool groups @@ -37,8 +32,8 @@ export const TOOL_GROUPS: Record = { research: { tools: ["search_web", "fetch_urls_content"], }, - transformations: { - tools: ["analyze_paper", "key_insights", "dense_summary", "reflections", "table_of_contents", "simple_summary"], + insights: { + tools: ["insights"], }, mcp: { tools: ["use_mcp_tool", "access_mcp_resource"], @@ -75,7 +70,7 @@ export const GROUP_DISPLAY_NAMES: Record = { read: "Read Files", edit: "Edit Files", research: "Research", - transformations: "Transformations", + insights: "insights", mcp: "MCP Tools", modes: "Modes", } diff --git a/src/database/database-manager.ts b/src/database/database-manager.ts index d5587eb..532c313 100644 --- a/src/database/database-manager.ts +++ b/src/database/database-manager.ts @@ -6,6 +6,7 @@ import { createAndInitDb } from '../pgworker' import { CommandManager } from './modules/command/command-manager' import { ConversationManager } from './modules/conversation/conversation-manager' +import { InsightManager } from './modules/insight/insight-manager' import { VectorManager } from './modules/vector/vector-manager' export class DBManager { @@ -14,6 +15,7 @@ export class DBManager { private vectorManager: VectorManager private CommandManager: CommandManager private conversationManager: ConversationManager + private insightManager: InsightManager constructor(app: App) { this.app = app @@ -26,6 +28,7 @@ export class DBManager { dbManager.vectorManager = new VectorManager(app, dbManager) dbManager.CommandManager = new CommandManager(app, dbManager) dbManager.conversationManager = new ConversationManager(app, dbManager) + dbManager.insightManager = new InsightManager(app, dbManager) return dbManager } @@ -46,6 +49,10 @@ export class DBManager { return this.conversationManager } + getInsightManager(): InsightManager { + return this.insightManager + } + async cleanup() { this.db?.close() this.db = null diff --git a/src/database/modules/insight/index.ts b/src/database/modules/insight/index.ts new file mode 100644 index 0000000..2e59cf2 --- /dev/null +++ b/src/database/modules/insight/index.ts @@ -0,0 +1,2 @@ +export { InsightRepository } from './insight-repository' +export { InsightManager } from './insight-manager' diff --git a/src/database/modules/insight/insight-manager.ts b/src/database/modules/insight/insight-manager.ts new file mode 100644 index 0000000..18d1f2c --- /dev/null +++ b/src/database/modules/insight/insight-manager.ts @@ -0,0 +1,321 @@ +import { App, TFile } from 'obsidian' + +import { InsertSourceInsight, SelectSourceInsight } from '../../schema' +import { EmbeddingModel } from '../../../types/embedding' +import { DBManager } from '../../database-manager' + +import { InsightRepository } from './insight-repository' + +export class InsightManager { + private app: App + private repository: InsightRepository + private dbManager: DBManager + + constructor(app: App, dbManager: DBManager) { + this.app = app + this.dbManager = dbManager + this.repository = new InsightRepository(app, dbManager.getPgClient()) + } + + /** + * 执行洞察相似性搜索 + */ + async performSimilaritySearch( + queryVector: number[], + embeddingModel: EmbeddingModel, + options: { + minSimilarity: number + limit: number + insightTypes?: string[] + sourceTypes?: ('document' | 'tag' | 'folder')[] + sourcePaths?: string[] + }, + ): Promise< + (Omit & { + similarity: number + })[] + > { + return await this.repository.performSimilaritySearch( + queryVector, + embeddingModel, + options, + ) + } + + /** + * 存储单个洞察 + */ + async storeInsight( + insightData: { + insightType: string + insight: string + sourceType: 'document' | 'tag' | 'folder' + sourcePath: string + embedding: number[] + }, + embeddingModel: EmbeddingModel, + ): Promise { + const insertData: InsertSourceInsight = { + insight_type: insightData.insightType, + insight: insightData.insight, + source_type: insightData.sourceType, + source_path: insightData.sourcePath, + embedding: insightData.embedding, + } + + await this.repository.insertInsights([insertData], embeddingModel) + } + + /** + * 批量存储洞察 + */ + async storeBatchInsights( + insightsData: Array<{ + insightType: string + insight: string + sourceType: 'document' | 'tag' | 'folder' + sourcePath: string + embedding: number[] + }>, + embeddingModel: EmbeddingModel, + ): Promise { + const insertData: InsertSourceInsight[] = insightsData.map(data => ({ + insight_type: data.insightType, + insight: data.insight, + source_type: data.sourceType, + source_path: data.sourcePath, + embedding: data.embedding, + })) + + await this.repository.insertInsights(insertData, embeddingModel) + } + + /** + * 更新现有洞察 + */ + async updateInsight( + id: number, + updates: { + insightType?: string + insight?: string + sourceType?: 'document' | 'tag' | 'folder' + sourcePath?: string + embedding?: number[] + }, + embeddingModel: EmbeddingModel, + ): Promise { + const updateData: Partial = {} + + if (updates.insightType !== undefined) { + updateData.insight_type = updates.insightType + } + if (updates.insight !== undefined) { + updateData.insight = updates.insight + } + if (updates.sourceType !== undefined) { + updateData.source_type = updates.sourceType + } + if (updates.sourcePath !== undefined) { + updateData.source_path = updates.sourcePath + } + if (updates.embedding !== undefined) { + updateData.embedding = updates.embedding + } + + await this.repository.updateInsight(id, updateData, embeddingModel) + } + + /** + * 获取所有洞察 + */ + async getAllInsights(embeddingModel: EmbeddingModel): Promise { + return await this.repository.getAllInsights(embeddingModel) + } + + /** + * 根据源路径获取洞察 + */ + async getInsightsBySourcePath( + sourcePath: string, + embeddingModel: EmbeddingModel, + ): Promise { + return await this.repository.getInsightsBySourcePath(sourcePath, embeddingModel) + } + + /** + * 根据洞察类型获取洞察 + */ + async getInsightsByType( + insightType: string, + embeddingModel: EmbeddingModel, + ): Promise { + return await this.repository.getInsightsByType(insightType, embeddingModel) + } + + /** + * 根据源类型获取洞察 + */ + async getInsightsBySourceType( + sourceType: 'document' | 'tag' | 'folder', + embeddingModel: EmbeddingModel, + ): Promise { + return await this.repository.getInsightsBySourceType(sourceType, embeddingModel) + } + + /** + * 删除指定源路径的所有洞察 + */ + async deleteInsightsBySourcePath( + sourcePath: string, + embeddingModel: EmbeddingModel, + ): Promise { + await this.repository.deleteInsightsBySourcePath(sourcePath, embeddingModel) + } + + /** + * 批量删除多个源路径的洞察 + */ + async deleteInsightsBySourcePaths( + sourcePaths: string[], + embeddingModel: EmbeddingModel, + ): Promise { + await this.repository.deleteInsightsBySourcePaths(sourcePaths, embeddingModel) + } + + /** + * 删除指定类型的所有洞察 + */ + async deleteInsightsByType( + insightType: string, + embeddingModel: EmbeddingModel, + ): Promise { + await this.repository.deleteInsightsByType(insightType, embeddingModel) + } + + /** + * 清空所有洞察 + */ + async clearAllInsights(embeddingModel: EmbeddingModel): Promise { + await this.repository.clearAllInsights(embeddingModel) + } + + /** + * 文件删除时清理相关洞察 + */ + async cleanInsightsForDeletedFile( + file: TFile, + embeddingModel: EmbeddingModel, + ): Promise { + await this.repository.deleteInsightsBySourcePath(file.path, embeddingModel) + } + + /** + * 文件重命名时更新洞察路径 + */ + async updateInsightsForRenamedFile( + oldPath: string, + newPath: string, + embeddingModel: EmbeddingModel, + ): Promise { + // 获取旧路径的所有洞察 + const insights = await this.repository.getInsightsBySourcePath(oldPath, embeddingModel) + + // 批量更新路径 + for (const insight of insights) { + await this.repository.updateInsight( + insight.id, + { source_path: newPath }, + embeddingModel + ) + } + } + + /** + * 清理已删除文件的洞察(批量清理) + */ + async cleanInsightsForDeletedFiles(embeddingModel: EmbeddingModel): Promise { + const allInsights = await this.repository.getAllInsights(embeddingModel) + const pathsToDelete: string[] = [] + + for (const insight of allInsights) { + if (insight.source_type === 'document') { + // 检查文件是否还存在 + const file = this.app.vault.getAbstractFileByPath(insight.source_path) + if (!file) { + pathsToDelete.push(insight.source_path) + } + } + } + + if (pathsToDelete.length > 0) { + await this.repository.deleteInsightsBySourcePaths(pathsToDelete, embeddingModel) + } + } + + /** + * 获取洞察统计信息 + */ + async getInsightStats(embeddingModel: EmbeddingModel): Promise<{ + total: number + byType: Record + bySourceType: Record + }> { + const allInsights = await this.repository.getAllInsights(embeddingModel) + + const stats = { + total: allInsights.length, + byType: {} as Record, + bySourceType: {} as Record, + } + + for (const insight of allInsights) { + // 统计洞察类型 + stats.byType[insight.insight_type] = (stats.byType[insight.insight_type] || 0) + 1 + + // 统计源类型 + stats.bySourceType[insight.source_type] = (stats.bySourceType[insight.source_type] || 0) + 1 + } + + return stats + } + + /** + * 搜索洞察(文本搜索,非向量搜索) + */ + async searchInsightsByText( + searchText: string, + embeddingModel: EmbeddingModel, + options?: { + insightTypes?: string[] + sourceTypes?: ('document' | 'tag' | 'folder')[] + limit?: number + } + ): Promise { + // 这里可以实现基于文本的搜索逻辑 + // 目前先返回所有洞察,然后在内存中过滤 + const allInsights = await this.repository.getAllInsights(embeddingModel) + + let filteredInsights = allInsights.filter(insight => + insight.insight.toLowerCase().includes(searchText.toLowerCase()) || + insight.insight_type.toLowerCase().includes(searchText.toLowerCase()) + ) + + if (options?.insightTypes) { + filteredInsights = filteredInsights.filter(insight => + options.insightTypes!.includes(insight.insight_type) + ) + } + + if (options?.sourceTypes) { + filteredInsights = filteredInsights.filter(insight => + options.sourceTypes!.includes(insight.source_type) + ) + } + + if (options?.limit) { + filteredInsights = filteredInsights.slice(0, options.limit) + } + + return filteredInsights + } +} diff --git a/src/database/modules/insight/insight-repository.ts b/src/database/modules/insight/insight-repository.ts new file mode 100644 index 0000000..d3bb37e --- /dev/null +++ b/src/database/modules/insight/insight-repository.ts @@ -0,0 +1,274 @@ +import { PGliteInterface } from '@electric-sql/pglite' +import { App } from 'obsidian' + +import { EmbeddingModel } from '../../../types/embedding' +import { DatabaseNotInitializedException } from '../../exception' +import { InsertSourceInsight, SelectSourceInsight, sourceInsightTables } from '../../schema' + +export class InsightRepository { + private app: App + private db: PGliteInterface | null + + constructor(app: App, pgClient: PGliteInterface | null) { + this.app = app + this.db = pgClient + } + + private getTableName(embeddingModel: EmbeddingModel): string { + const tableDefinition = sourceInsightTables[embeddingModel.dimension] + if (!tableDefinition) { + throw new Error(`No source insight table definition found for model: ${embeddingModel.id}`) + } + return tableDefinition.name + } + + async getAllInsights(embeddingModel: EmbeddingModel): Promise { + if (!this.db) { + throw new DatabaseNotInitializedException() + } + const tableName = this.getTableName(embeddingModel) + const result = await this.db.query( + `SELECT * FROM "${tableName}" ORDER BY created_at DESC` + ) + return result.rows + } + + async getInsightsBySourcePath( + sourcePath: string, + embeddingModel: EmbeddingModel, + ): Promise { + if (!this.db) { + throw new DatabaseNotInitializedException() + } + const tableName = this.getTableName(embeddingModel) + const result = await this.db.query( + `SELECT * FROM "${tableName}" WHERE source_path = $1 ORDER BY created_at DESC`, + [sourcePath] + ) + return result.rows + } + + async getInsightsByType( + insightType: string, + embeddingModel: EmbeddingModel, + ): Promise { + if (!this.db) { + throw new DatabaseNotInitializedException() + } + const tableName = this.getTableName(embeddingModel) + const result = await this.db.query( + `SELECT * FROM "${tableName}" WHERE insight_type = $1 ORDER BY created_at DESC`, + [insightType] + ) + return result.rows + } + + async getInsightsBySourceType( + sourceType: 'document' | 'tag' | 'folder', + embeddingModel: EmbeddingModel, + ): Promise { + if (!this.db) { + throw new DatabaseNotInitializedException() + } + const tableName = this.getTableName(embeddingModel) + const result = await this.db.query( + `SELECT * FROM "${tableName}" WHERE source_type = $1 ORDER BY created_at DESC`, + [sourceType] + ) + return result.rows + } + + async deleteInsightsBySourcePath( + sourcePath: string, + embeddingModel: EmbeddingModel, + ): Promise { + if (!this.db) { + throw new DatabaseNotInitializedException() + } + const tableName = this.getTableName(embeddingModel) + await this.db.query( + `DELETE FROM "${tableName}" WHERE source_path = $1`, + [sourcePath] + ) + } + + async deleteInsightsBySourcePaths( + sourcePaths: string[], + embeddingModel: EmbeddingModel, + ): Promise { + if (!this.db) { + throw new DatabaseNotInitializedException() + } + const tableName = this.getTableName(embeddingModel) + await this.db.query( + `DELETE FROM "${tableName}" WHERE source_path = ANY($1)`, + [sourcePaths] + ) + } + + async deleteInsightsByType( + insightType: string, + embeddingModel: EmbeddingModel, + ): Promise { + if (!this.db) { + throw new DatabaseNotInitializedException() + } + const tableName = this.getTableName(embeddingModel) + await this.db.query( + `DELETE FROM "${tableName}" WHERE insight_type = $1`, + [insightType] + ) + } + + async clearAllInsights(embeddingModel: EmbeddingModel): Promise { + if (!this.db) { + throw new DatabaseNotInitializedException() + } + const tableName = this.getTableName(embeddingModel) + await this.db.query(`DELETE FROM "${tableName}"`) + } + + async insertInsights( + data: InsertSourceInsight[], + embeddingModel: EmbeddingModel, + ): Promise { + if (!this.db) { + throw new DatabaseNotInitializedException() + } + const tableName = this.getTableName(embeddingModel) + + // 构建批量插入的 SQL + const values = data.map((insight, index) => { + const offset = index * 6 + return `($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4}, $${offset + 5}, $${offset + 6})` + }).join(',') + + const params = data.flatMap(insight => [ + insight.insight_type, + insight.insight.replace(/\0/g, ''), // 清理null字节 + insight.source_type, + insight.source_path, + `[${insight.embedding.join(',')}]`, // 转换为PostgreSQL vector格式 + new Date() // updated_at + ]) + + await this.db.query( + `INSERT INTO "${tableName}" (insight_type, insight, source_type, source_path, embedding, updated_at) + VALUES ${values}`, + params + ) + } + + async updateInsight( + id: number, + data: Partial, + embeddingModel: EmbeddingModel, + ): Promise { + if (!this.db) { + throw new DatabaseNotInitializedException() + } + const tableName = this.getTableName(embeddingModel) + + const fields: string[] = [] + const params: unknown[] = [] + let paramIndex = 1 + + if (data.insight_type !== undefined) { + fields.push(`insight_type = $${paramIndex}`) + params.push(data.insight_type) + paramIndex++ + } + + if (data.insight !== undefined) { + fields.push(`insight = $${paramIndex}`) + params.push(data.insight.replace(/\0/g, '')) + paramIndex++ + } + + if (data.source_type !== undefined) { + fields.push(`source_type = $${paramIndex}`) + params.push(data.source_type) + paramIndex++ + } + + if (data.source_path !== undefined) { + fields.push(`source_path = $${paramIndex}`) + params.push(data.source_path) + paramIndex++ + } + + if (data.embedding !== undefined) { + fields.push(`embedding = $${paramIndex}`) + params.push(`[${data.embedding.join(',')}]`) + paramIndex++ + } + + fields.push(`updated_at = $${paramIndex}`) + params.push(new Date()) + paramIndex++ + + params.push(id) + + await this.db.query( + `UPDATE "${tableName}" SET ${fields.join(', ')} WHERE id = $${paramIndex}`, + params + ) + } + + async performSimilaritySearch( + queryVector: number[], + embeddingModel: EmbeddingModel, + options: { + minSimilarity: number + limit: number + insightTypes?: string[] + sourceTypes?: ('document' | 'tag' | 'folder')[] + sourcePaths?: string[] + }, + ): Promise< + (Omit & { + similarity: number + })[] + > { + if (!this.db) { + throw new DatabaseNotInitializedException() + } + const tableName = this.getTableName(embeddingModel) + + let whereConditions = ['1 - (embedding <=> $1::vector) > $2'] + const params: unknown[] = [`[${queryVector.join(',')}]`, options.minSimilarity, options.limit] + let paramIndex = 4 + + if (options.insightTypes && options.insightTypes.length > 0) { + whereConditions.push(`insight_type = ANY($${paramIndex})`) + params.push(options.insightTypes) + paramIndex++ + } + + if (options.sourceTypes && options.sourceTypes.length > 0) { + whereConditions.push(`source_type = ANY($${paramIndex})`) + params.push(options.sourceTypes) + paramIndex++ + } + + if (options.sourcePaths && options.sourcePaths.length > 0) { + whereConditions.push(`source_path = ANY($${paramIndex})`) + params.push(options.sourcePaths) + paramIndex++ + } + + const query = ` + SELECT + id, insight_type, insight, source_type, source_path, created_at, updated_at, + 1 - (embedding <=> $1::vector) as similarity + FROM "${tableName}" + WHERE ${whereConditions.join(' AND ')} + ORDER BY similarity DESC + LIMIT $3 + ` + + type SearchResult = Omit & { similarity: number } + const result = await this.db.query(query, params) + return result.rows + } +} diff --git a/src/database/modules/vector/vector-repository.ts b/src/database/modules/vector/vector-repository.ts index 5068f3a..435c93e 100644 --- a/src/database/modules/vector/vector-repository.ts +++ b/src/database/modules/vector/vector-repository.ts @@ -136,7 +136,7 @@ export class VectorRepository { const tableName = this.getTableName(embeddingModel) let scopeCondition = '' - const params: any[] = [`[${queryVector.join(',')}]`, options.minSimilarity, options.limit] + const params: unknown[] = [`[${queryVector.join(',')}]`, options.minSimilarity, options.limit] let paramIndex = 4 if (options.scope) { diff --git a/src/database/schema.ts b/src/database/schema.ts index 48090c5..6d98ba0 100644 --- a/src/database/schema.ts +++ b/src/database/schema.ts @@ -176,3 +176,63 @@ export type SelectMessage = { similarity_search_results?: string | null created_at: Date } + +/* Source Insight Table */ +export type SourceInsightRecord = { + id: number + insight_type: string + insight: string + source_type: 'document' | 'tag' | 'folder' + source_path: string + embedding: number[] + created_at: Date + updated_at: Date +} + +export type SelectSourceInsight = SourceInsightRecord +export type InsertSourceInsight = Omit + +const createSourceInsightTable = (dimension: number): TableDefinition => { + const tableName = `source_insight_${dimension}` + + const table: TableDefinition = { + name: tableName, + columns: { + id: { type: 'SERIAL', primaryKey: true }, + insight_type: { type: 'TEXT', notNull: true }, + insight: { type: 'TEXT', notNull: true }, + source_type: { type: 'TEXT', notNull: true }, + source_path: { type: 'TEXT', notNull: true }, + embedding: { type: 'VECTOR', dimensions: dimension }, + created_at: { type: 'TIMESTAMP', notNull: true, defaultNow: true }, + updated_at: { type: 'TIMESTAMP', notNull: true, defaultNow: true } + } + } + + if (dimension <= 2000) { + table.indices = { + [`insightEmbeddingIndex_${dimension}`]: { + type: 'HNSW', + columns: ['embedding'], + options: 'vector_cosine_ops' + }, + [`insightSourceIndex_${dimension}`]: { + type: 'BTREE', + columns: ['source_path'] + }, + [`insightTypeIndex_${dimension}`]: { + type: 'BTREE', + columns: ['insight_type'] + } + } + } + + return table +} + +export const sourceInsightTables = SUPPORT_EMBEDDING_SIMENTION.reduce< + Record +>((acc, dimension) => { + acc[dimension] = createSourceInsightTable(dimension) + return acc +}, {}) diff --git a/src/database/sql.ts b/src/database/sql.ts index 8f61434..c263baa 100644 --- a/src/database/sql.ts +++ b/src/database/sql.ts @@ -94,6 +94,119 @@ export const migrations: Record = { ON "embeddings_384" ("path"); ` }, + source_insight: { + description: "Creates source insight tables and indexes for different embedding models", + sql: ` + -- Create source insight tables for different embedding dimensions + CREATE TABLE IF NOT EXISTS "source_insight_1536" ( + "id" serial PRIMARY KEY NOT NULL, + "insight_type" text NOT NULL, + "insight" text NOT NULL, + "source_type" text NOT NULL, + "source_path" text NOT NULL, + "embedding" vector(1536), + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL + ); + + CREATE TABLE IF NOT EXISTS "source_insight_1024" ( + "id" serial PRIMARY KEY NOT NULL, + "insight_type" text NOT NULL, + "insight" text NOT NULL, + "source_type" text NOT NULL, + "source_path" text NOT NULL, + "embedding" vector(1024), + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL + ); + + CREATE TABLE IF NOT EXISTS "source_insight_768" ( + "id" serial PRIMARY KEY NOT NULL, + "insight_type" text NOT NULL, + "insight" text NOT NULL, + "source_type" text NOT NULL, + "source_path" text NOT NULL, + "embedding" vector(768), + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL + ); + + CREATE TABLE IF NOT EXISTS "source_insight_512" ( + "id" serial PRIMARY KEY NOT NULL, + "insight_type" text NOT NULL, + "insight" text NOT NULL, + "source_type" text NOT NULL, + "source_path" text NOT NULL, + "embedding" vector(512), + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL + ); + + CREATE TABLE IF NOT EXISTS "source_insight_384" ( + "id" serial PRIMARY KEY NOT NULL, + "insight_type" text NOT NULL, + "insight" text NOT NULL, + "source_type" text NOT NULL, + "source_path" text NOT NULL, + "embedding" vector(384), + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL + ); + + -- Create HNSW indexes for embedding similarity search + CREATE INDEX IF NOT EXISTS "insightEmbeddingIndex_1536" + ON "source_insight_1536" + USING hnsw ("embedding" vector_cosine_ops); + + CREATE INDEX IF NOT EXISTS "insightEmbeddingIndex_1024" + ON "source_insight_1024" + USING hnsw ("embedding" vector_cosine_ops); + + CREATE INDEX IF NOT EXISTS "insightEmbeddingIndex_768" + ON "source_insight_768" + USING hnsw ("embedding" vector_cosine_ops); + + CREATE INDEX IF NOT EXISTS "insightEmbeddingIndex_512" + ON "source_insight_512" + USING hnsw ("embedding" vector_cosine_ops); + + CREATE INDEX IF NOT EXISTS "insightEmbeddingIndex_384" + ON "source_insight_384" + USING hnsw ("embedding" vector_cosine_ops); + + -- Create B-tree indexes for source_path field + CREATE INDEX IF NOT EXISTS "insightSourceIndex_1536" + ON "source_insight_1536" ("source_path"); + + CREATE INDEX IF NOT EXISTS "insightSourceIndex_1024" + ON "source_insight_1024" ("source_path"); + + CREATE INDEX IF NOT EXISTS "insightSourceIndex_768" + ON "source_insight_768" ("source_path"); + + CREATE INDEX IF NOT EXISTS "insightSourceIndex_512" + ON "source_insight_512" ("source_path"); + + CREATE INDEX IF NOT EXISTS "insightSourceIndex_384" + ON "source_insight_384" ("source_path"); + + -- Create B-tree indexes for insight_type field + CREATE INDEX IF NOT EXISTS "insightTypeIndex_1536" + ON "source_insight_1536" ("insight_type"); + + CREATE INDEX IF NOT EXISTS "insightTypeIndex_1024" + ON "source_insight_1024" ("insight_type"); + + CREATE INDEX IF NOT EXISTS "insightTypeIndex_768" + ON "source_insight_768" ("insight_type"); + + CREATE INDEX IF NOT EXISTS "insightTypeIndex_512" + ON "source_insight_512" ("insight_type"); + + CREATE INDEX IF NOT EXISTS "insightTypeIndex_384" + ON "source_insight_384" ("insight_type"); + ` + }, template: { description: "Creates template table with UUID support", sql: ` diff --git a/src/types/apply.ts b/src/types/apply.ts index d0787be..6ab560b 100644 --- a/src/types/apply.ts +++ b/src/types/apply.ts @@ -112,43 +112,11 @@ export type DataviewQueryToolArgs = { finish?: boolean; } -export type AnalyzePaperToolArgs = { - type: 'analyze_paper'; +export type CallTransformationsToolArgs = { + type: 'call_transformations'; path: string; + transformation: string; finish?: boolean; } -export type KeyInsightsToolArgs = { - type: 'key_insights'; - path: string; - finish?: boolean; -} - -export type DenseSummaryToolArgs = { - type: 'dense_summary'; - path: string; - finish?: boolean; -} - -export type ReflectionsToolArgs = { - type: 'reflections'; - path: string; - finish?: boolean; -} - -export type TableOfContentsToolArgs = { - type: 'table_of_contents'; - path: string; - depth?: number; - format?: string; - include_summary?: boolean; - finish?: boolean; -} - -export type SimpleSummaryToolArgs = { - type: 'simple_summary'; - path: string; - finish?: boolean; -} - -export type ToolArgs = ReadFileToolArgs | WriteToFileToolArgs | InsertContentToolArgs | SearchAndReplaceToolArgs | ListFilesToolArgs | MatchSearchFilesToolArgs | RegexSearchFilesToolArgs | SemanticSearchFilesToolArgs | SearchWebToolArgs | FetchUrlsContentToolArgs | SwitchModeToolArgs | ApplyDiffToolArgs | UseMcpToolArgs | DataviewQueryToolArgs | AnalyzePaperToolArgs | KeyInsightsToolArgs | DenseSummaryToolArgs | ReflectionsToolArgs | TableOfContentsToolArgs | SimpleSummaryToolArgs; +export type ToolArgs = ReadFileToolArgs | WriteToFileToolArgs | InsertContentToolArgs | SearchAndReplaceToolArgs | ListFilesToolArgs | MatchSearchFilesToolArgs | RegexSearchFilesToolArgs | SemanticSearchFilesToolArgs | SearchWebToolArgs | FetchUrlsContentToolArgs | SwitchModeToolArgs | ApplyDiffToolArgs | UseMcpToolArgs | DataviewQueryToolArgs | CallTransformationsToolArgs; diff --git a/src/utils/modes.ts b/src/utils/modes.ts index 6c5293c..9930bf4 100644 --- a/src/utils/modes.ts +++ b/src/utils/modes.ts @@ -88,8 +88,8 @@ export const defaultModes: ModeConfig[] = [ slug: "ask", name: "Ask", roleDefinition: - "You are Infio, an AI knowledge assistant powered by advanced language models. You operate within Obsidian.", - groups: ["read", "transformations", "mcp"], + "You are an AI knowledge vault researcher. Your core mission is to deeply explore the user's Obsidian knowledge vault, understand their questions, find the most relevant information, and synthesize clear, evidence-based answers. You are not a general chatbot; every response must be grounded in the user's note contents. Treat each question as a micro-research task.", + groups: ["read", "insights", "mcp"], customInstructions: "You are collaborating with a USER to help them explore, understand, and organize information within their personal knowledge vault. Each time the USER sends a message, they may provide context about their current notes, vault structure, or specific knowledge needs. This information may or may not be relevant to their inquiry, it is up for you to decide.\n\nYour main goal is to provide informative responses, thoughtful explanations, and practical guidance on any topic or challenge they face, while leveraging their existing knowledge base when relevant. You can analyze information, explain concepts across various domains, and access external resources when helpful. Make sure to address the user's questions thoroughly with thoughtful explanations and practical guidance. Use visual aids like Mermaid diagrams when they help make complex topics clearer. Offer solutions to challenges from diverse fields, not just technical ones, and provide context that helps users better understand the subject matter.", }, @@ -107,7 +107,7 @@ export const defaultModes: ModeConfig[] = [ name: "Learn", roleDefinition: "You are Infio, an AI learning assistant powered by advanced language models. You operate within Obsidian.", - groups: ["read", "transformations", "mcp"], + groups: ["read", "insights", "mcp"], customInstructions: "You are collaborating with a USER to enhance their learning experience within their knowledge vault. Each time the USER sends a message, they may provide context about their learning materials, study goals, or knowledge gaps. This information may or may not be relevant to their learning journey, it is up for you to decide.\n\nYour main goal is to help them actively learn and understand complex topics by transforming information into more digestible formats, creating connections between concepts, and facilitating deep comprehension. You excel at breaking down complex topics into manageable chunks, creating study materials like flashcards and summaries, and helping users build comprehensive understanding through structured learning approaches. Generate visual learning aids like concept maps and flowcharts using Mermaid diagrams when they enhance comprehension. Create organized study materials including key concepts, definitions, and practice questions. Help users connect new information to their existing knowledge base by identifying relationships and patterns across their notes. Focus on active learning techniques that promote retention and understanding rather than passive information consumption." }, diff --git a/src/utils/parse-infio-block.ts b/src/utils/parse-infio-block.ts index 6f5b6f1..79df520 100644 --- a/src/utils/parse-infio-block.ts +++ b/src/utils/parse-infio-block.ts @@ -98,36 +98,14 @@ export type ParsedMsgBlock = query: string outputFormat: string finish: boolean + } | { + type: 'call_transformations' + path: string + transformation: string + finish: boolean } | { type: 'tool_result' content: string - } | { - type: 'analyze_paper' - path: string - finish: boolean - } | { - type: 'key_insights' - path: string - finish: boolean - } | { - type: 'dense_summary' - path: string - finish: boolean - } | { - type: 'reflections' - path: string - finish: boolean - } | { - type: 'table_of_contents' - path: string - depth?: number - format?: string - include_summary?: boolean - finish: boolean - } | { - type: 'simple_summary' - path: string - finish: boolean } export function parseMsgBlocks( @@ -739,7 +717,7 @@ export function parseMsgBlocks( finish: node.sourceCodeLocation.endTag !== undefined }) lastEndOffset = endOffset - } else if (node.nodeName === 'analyze_paper') { + } else if (node.nodeName === 'insights') { if (!node.sourceCodeLocation) { throw new Error('sourceCodeLocation is undefined') } @@ -752,159 +730,22 @@ export function parseMsgBlocks( }) } let path: string | undefined + let transformation: string | undefined + for (const childNode of node.childNodes) { if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) { // @ts-expect-error - parse5 node value type path = childNode.childNodes[0].value + } else if (childNode.nodeName === 'type' && childNode.childNodes.length > 0) { + // @ts-expect-error - parse5 node value type + transformation = childNode.childNodes[0].value } } + parsedResult.push({ - type: 'analyze_paper', - path: path || '', - finish: node.sourceCodeLocation.endTag !== undefined - }) - lastEndOffset = endOffset - } else if (node.nodeName === 'key_insights') { - if (!node.sourceCodeLocation) { - throw new Error('sourceCodeLocation is undefined') - } - const startOffset = node.sourceCodeLocation.startOffset - const endOffset = node.sourceCodeLocation.endOffset - if (startOffset > lastEndOffset) { - parsedResult.push({ - type: 'string', - content: input.slice(lastEndOffset, startOffset), - }) - } - let path: string | undefined - for (const childNode of node.childNodes) { - if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) { - // @ts-expect-error - parse5 node value type - path = childNode.childNodes[0].value - } - } - parsedResult.push({ - type: 'key_insights', - path: path || '', - finish: node.sourceCodeLocation.endTag !== undefined - }) - lastEndOffset = endOffset - } else if (node.nodeName === 'dense_summary') { - if (!node.sourceCodeLocation) { - throw new Error('sourceCodeLocation is undefined') - } - const startOffset = node.sourceCodeLocation.startOffset - const endOffset = node.sourceCodeLocation.endOffset - if (startOffset > lastEndOffset) { - parsedResult.push({ - type: 'string', - content: input.slice(lastEndOffset, startOffset), - }) - } - let path: string | undefined - for (const childNode of node.childNodes) { - if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) { - // @ts-expect-error - parse5 node value type - path = childNode.childNodes[0].value - } - } - parsedResult.push({ - type: 'dense_summary', - path: path || '', - finish: node.sourceCodeLocation.endTag !== undefined - }) - lastEndOffset = endOffset - } else if (node.nodeName === 'reflections') { - if (!node.sourceCodeLocation) { - throw new Error('sourceCodeLocation is undefined') - } - const startOffset = node.sourceCodeLocation.startOffset - const endOffset = node.sourceCodeLocation.endOffset - if (startOffset > lastEndOffset) { - parsedResult.push({ - type: 'string', - content: input.slice(lastEndOffset, startOffset), - }) - } - let path: string | undefined - for (const childNode of node.childNodes) { - if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) { - // @ts-expect-error - parse5 node value type - path = childNode.childNodes[0].value - } - } - parsedResult.push({ - type: 'reflections', - path: path || '', - finish: node.sourceCodeLocation.endTag !== undefined - }) - lastEndOffset = endOffset - } else if (node.nodeName === 'table_of_contents') { - if (!node.sourceCodeLocation) { - throw new Error('sourceCodeLocation is undefined') - } - const startOffset = node.sourceCodeLocation.startOffset - const endOffset = node.sourceCodeLocation.endOffset - if (startOffset > lastEndOffset) { - parsedResult.push({ - type: 'string', - content: input.slice(lastEndOffset, startOffset), - }) - } - let path: string | undefined - let depth: number | undefined - let format: string | undefined - let include_summary: boolean | undefined - - for (const childNode of node.childNodes) { - if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) { - // @ts-expect-error - parse5 node value type - path = childNode.childNodes[0].value - } else if (childNode.nodeName === 'depth' && childNode.childNodes.length > 0) { - // @ts-expect-error - parse5 node value type - const depthStr = childNode.childNodes[0].value - depth = depthStr ? parseInt(depthStr as string) : undefined - } else if (childNode.nodeName === 'format' && childNode.childNodes.length > 0) { - // @ts-expect-error - parse5 node value type - format = childNode.childNodes[0].value - } else if (childNode.nodeName === 'include_summary' && childNode.childNodes.length > 0) { - // @ts-expect-error - parse5 node value type - const summaryValue = childNode.childNodes[0].value - include_summary = summaryValue ? (summaryValue as string).toLowerCase() === 'true' : false - } - } - - parsedResult.push({ - type: 'table_of_contents', - path: path || '', - depth, - format, - include_summary, - finish: node.sourceCodeLocation.endTag !== undefined - }) - lastEndOffset = endOffset - } else if (node.nodeName === 'simple_summary') { - if (!node.sourceCodeLocation) { - throw new Error('sourceCodeLocation is undefined') - } - const startOffset = node.sourceCodeLocation.startOffset - const endOffset = node.sourceCodeLocation.endOffset - if (startOffset > lastEndOffset) { - parsedResult.push({ - type: 'string', - content: input.slice(lastEndOffset, startOffset), - }) - } - let path: string | undefined - for (const childNode of node.childNodes) { - if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) { - // @ts-expect-error - parse5 node value type - path = childNode.childNodes[0].value - } - } - parsedResult.push({ - type: 'simple_summary', + type: 'call_transformations', path: path || '', + transformation: transformation || '', finish: node.sourceCodeLocation.endTag !== undefined }) lastEndOffset = endOffset diff --git a/src/utils/prompt-generator.ts b/src/utils/prompt-generator.ts index 86ffcf9..f52bf02 100644 --- a/src/utils/prompt-generator.ts +++ b/src/utils/prompt-generator.ts @@ -8,6 +8,7 @@ import { SystemPrompt } from '../core/prompts/system' import { RAGEngine } from '../core/rag/rag-engine' import { ConvertDataManager } from '../database/json/convert-data/ConvertDataManager' import { ConvertType } from '../database/json/convert-data/types' +import { WorkspaceManager } from '../database/json/workspace/WorkspaceManager' import { SelectVector } from '../database/schema' import { ChatMessage, ChatUserMessage } from '../types/chat' import { ContentPart, RequestMessage } from '../types/llm/request' @@ -134,6 +135,13 @@ async function getFileOrFolderContent( } } +function formatSection(title: string, content: string | null | undefined): string { + if (!content || content.trim() === '') { + return '' + } + return `\n\n# ${title}\n${content.trim()}` +} + export class PromptGenerator { private getRagEngine: () => Promise private app: App @@ -144,6 +152,7 @@ export class PromptGenerator { private customModeList: ModeConfig[] | null = null private getMcpHub: () => Promise | null = null private convertDataManager: ConvertDataManager + private workspaceManager: WorkspaceManager private static readonly EMPTY_ASSISTANT_MESSAGE: RequestMessage = { role: 'assistant', content: '', @@ -167,6 +176,7 @@ export class PromptGenerator { this.customModeList = customModeList ?? null this.getMcpHub = getMcpHub ?? null this.convertDataManager = new ConvertDataManager(app) + this.workspaceManager = new WorkspaceManager(app) } public async generateRequestMessages({ @@ -243,72 +253,154 @@ export class PromptGenerator { } } - private async getEnvironmentDetails() { - let details = "" - // Obsidian Current File - details += "\n\n# Obsidian Current File" - const currentFile = this.app.workspace.getActiveFile() - if (currentFile) { - details += `\n${currentFile?.path}` - } else { - details += "\n(No current file)" + private async getEnvironmentDetails(): Promise { + const currentNoteContext = await this.getCurrentNoteContext() + const noteConnectivity = await this.getNoteConnectivity() + const workspaceOverview = await this.getWorkspaceOverview() + const assistantState = await this.getAssistantState() + + const details = [ + currentNoteContext, + noteConnectivity, + workspaceOverview, + assistantState, + ] + .filter(Boolean) + .join('') + + if (!details.trim()) { + return '' } - // Obsidian Open Tabs - details += "\n\n# Obsidian Open Tabs" - const openTabs: string[] = []; - this.app.workspace.iterateAllLeaves(leaf => { - if (leaf.view instanceof MarkdownView && leaf.view.file) { - openTabs.push(leaf.view.file?.path); - } - }); - if (openTabs.length === 0) { - details += "\n(No open tabs)" - } else { - details += `\n${openTabs.join("\n")}` - } - - // Add current time information with timezone - const now = new Date() - const formatter = new Intl.DateTimeFormat(undefined, { - year: "numeric", - month: "numeric", - day: "numeric", - hour: "numeric", - minute: "numeric", - second: "numeric", - hour12: true, - }) - const timeZone = formatter.resolvedOptions().timeZone - const timeZoneOffset = -now.getTimezoneOffset() / 60 // Convert to hours and invert sign to match conventional notation - const timeZoneOffsetStr = `${timeZoneOffset >= 0 ? "+" : ""}${timeZoneOffset}:00` - details += `\n\n# Current Time\n${formatter.format(now)} (${timeZone}, UTC${timeZoneOffsetStr})` - - // Add current mode details - const currentMode = this.settings.mode - const modeDetails = await getFullModeDetails(this.app, currentMode, this.customModeList, this.customModePrompts) - details += `\n\n# Current Mode\n` - details += `${currentMode}\n` - details += `${modeDetails.name}\n` - - // // Obsidian Current Folder - // const currentFolder = this.app.workspace.getActiveFile() ? this.app.workspace.getActiveFile()?.parent?.path : "/" - // // Obsidian Vault Files and Folders - // if (currentFolder) { - // details += `\n\n# Obsidian Current Folder (${currentFolder}) Files` - // const filesAndFolders = await listFilesAndFolders(this.app.vault, currentFolder) - // if (filesAndFolders.length > 0) { - // details += `\n${filesAndFolders.filter(Boolean).join("\n")}` - // } else { - // details += "\n(No Markdown files in current folder)" - // } - // } else { - // details += "\n(No current folder)" - // } - return `\n${details.trim()}\n` } + private async getCurrentNoteContext(): Promise { + const currentNote = this.app.workspace.getActiveFile() + if (!currentNote) { + return formatSection("Obsidian Current Note", "(No current note)") + } + + const fileCache = this.app.metadataCache.getFileCache(currentNote) + if (!fileCache) { + return formatSection("Obsidian Current Note", currentNote.path) + } + + let context = `Note Path: ${currentNote.path}` + + if (fileCache.frontmatter) { + const frontmatterString = Object.entries(fileCache.frontmatter) + .filter(([key]) => key !== 'position') + .map(([key, value]) => `${key}: ${JSON.stringify(value)}`) + .join('\n') + if (frontmatterString) { + context += `\n\n## Metadata (Frontmatter)\n${frontmatterString}` + } + } + + if (fileCache.headings && fileCache.headings.length > 0) { + const outline = fileCache.headings + .map(h => `${' '.repeat(h.level - 1)}- ${h.heading}`) + .join('\n') + context += `\n\n## Outline\n${outline}` + } + + return formatSection("Current Note Context", context) + } + + private async getNoteConnectivity(): Promise { + const currentFile = this.app.workspace.getActiveFile() + if (!currentFile) { + return null + } + + const fileCache = this.app.metadataCache.getFileCache(currentFile) + if (!fileCache) { + return null + } + + let connectivity = "" + + if (fileCache.links && fileCache.links.length > 0) { + const outgoingLinks = fileCache.links.map(l => `- [[${l.link}]]`).join('\n') + connectivity += `\n## Outgoing Links\n${outgoingLinks}` + } + + const backlinks: string[] = [] + const resolvedLinks = this.app.metadataCache.resolvedLinks + if (resolvedLinks) { + for (const sourcePath in resolvedLinks) { + if (currentFile.path in resolvedLinks[sourcePath]) { + backlinks.push(`- [[${sourcePath}]]`) + } + } + } + + if (backlinks.length > 0) { + connectivity += `\n\n## Backlinks\n${backlinks.join('\n')}` + } + + return formatSection("Note Connectivity", connectivity.trim()) + } + + private async getWorkspaceOverview(): Promise { + let overview = '' + + const currentWorkspaceName = this.settings.workspace + if (currentWorkspaceName && currentWorkspaceName !== 'vault') { + const workspace = await this.workspaceManager.findByName(currentWorkspaceName) + if (workspace) { + overview += `\n\n## Current Workspace\n${workspace.name}` + } + } else { + overview += `\n\n## Current Workspace\n${this.app.vault.getName()} (entire vault)` + } + + const recentFiles = this.app.vault.getMarkdownFiles() + .sort((a, b) => b.stat.mtime - a.stat.mtime) + .slice(0, 5) + .map(f => `- ${f.path}`) + .join('\n') + if (recentFiles) { + overview += `\n\n## Recently Edited Notes\n${recentFiles}` + } + + // @ts-ignore - getTags() is not in the public API but is widely used. + const tags = this.app.metadataCache.getTags() + const sortedTags = Object.entries(tags) + .sort(([, a], [, b]) => b - a) + .slice(0, 20) + .map(([tag, count]) => `- ${tag} (${count})`) + .join('\n') + if (sortedTags) { + overview += `\n\n## Global Tag Cloud (Top 20)\n${sortedTags}` + } + + return formatSection('Workspace Overview', overview) + } + + private async getAssistantState(): Promise { + let state = '' + + const now = new Date() + const formatter = new Intl.DateTimeFormat(undefined, { + year: "numeric", month: "numeric", day: "numeric", + hour: "numeric", minute: "numeric", second: "numeric", hour12: true, + }) + const timeZone = formatter.resolvedOptions().timeZone + const timeZoneOffset = -now.getTimezoneOffset() / 60 + const timeZoneOffsetStr = `${timeZoneOffset >= 0 ? "+" : ""}${timeZoneOffset}:00` + const timeDetails = `${formatter.format(now)} (${timeZone}, UTC${timeZoneOffsetStr})` + state += `\n## Current Time\n${timeDetails}` + + const currentMode = this.settings.mode + const modeDetails = await getFullModeDetails(this.app, currentMode, this.customModeList, this.customModePrompts) + const modeInfo = `${currentMode}\n${modeDetails.name}` + state += `\n\n## Current Mode\n${modeInfo}` + + return formatSection('Assistant & User State', state.trim()) + } + private async compileUserMessagePrompt({ isNewChat, message, diff --git a/src/utils/tool-groups.ts b/src/utils/tool-groups.ts index 89e55fc..a2f7d09 100644 --- a/src/utils/tool-groups.ts +++ b/src/utils/tool-groups.ts @@ -37,8 +37,8 @@ export const TOOL_GROUPS: Record = { research: { tools: ["search_web", "fetch_urls_content"], }, - transformations: { - tools: ["analyze_paper", "key_insights", "dense_summary", "reflections", "table_of_contents", "simple_summary"], + insights: { + tools: ["insights"], }, mcp: { tools: ["use_mcp_tool", "access_mcp_resource"],