fix trans tool

This commit is contained in:
duanfuxiang 2025-06-29 08:28:50 +08:00
parent 772270863c
commit f3a0252ab6
25 changed files with 1173 additions and 441 deletions

View File

@ -829,41 +829,25 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
mentionables: [], mentionables: [],
} }
} }
} else if (toolArgs.type === 'analyze_paper' || } else if (toolArgs.type === 'call_transformations') {
toolArgs.type === 'key_insights' || // Handling for the unified transformations tool
toolArgs.type === 'dense_summary' ||
toolArgs.type === 'reflections' ||
toolArgs.type === 'table_of_contents' ||
toolArgs.type === 'simple_summary') {
// 处理文档转换工具
console.log('toolArgs', toolArgs)
try { try {
// 获取文件 const targetFile = app.vault.getFileByPath(toolArgs.path);
const targetFile = app.vault.getFileByPath(toolArgs.path)
if (!targetFile) { if (!targetFile) {
throw new Error(`文件未找到: ${toolArgs.path}`) throw new Error(`File not found: ${toolArgs.path}`);
} }
// 读取文件内容 const fileContent = await readTFileContentPdf(targetFile, app.vault, app);
const fileContent = await readTFileContentPdf(targetFile, app.vault, app)
// The transformation type is now passed directly in the arguments
// 映射工具类型到转换类型 const transformationType = toolArgs.transformation as TransformationType;
const transformationTypeMap: Record<string, TransformationType> = {
'analyze_paper': TransformationType.ANALYZE_PAPER, // Validate that the transformation type is a valid enum member
'key_insights': TransformationType.KEY_INSIGHTS, if (!Object.values(TransformationType).includes(transformationType)) {
'dense_summary': TransformationType.DENSE_SUMMARY, throw new Error(`Unsupported transformation type: ${transformationType}`);
'reflections': TransformationType.REFLECTIONS,
'table_of_contents': TransformationType.TABLE_OF_CONTENTS,
'simple_summary': TransformationType.SIMPLE_SUMMARY
} }
const transformationType = transformationTypeMap[toolArgs.type] // Execute the transformation
if (!transformationType) {
throw new Error(`不支持的转换类型: ${toolArgs.type}`)
}
// 执行转换
const transformationResult = await runTransformation({ const transformationResult = await runTransformation({
content: fileContent, content: fileContent,
transformationType, transformationType,
@ -872,18 +856,17 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
provider: settings.applyModelProvider, provider: settings.applyModelProvider,
modelId: settings.applyModelId, modelId: settings.applyModelId,
} }
}) });
if (!transformationResult.success) { if (!transformationResult.success) {
throw new Error(transformationResult.error || '转换失败') throw new Error(transformationResult.error || 'Transformation failed');
} }
// 构建结果消息 // Build the result message
let formattedContent = `[${toolArgs.type}] 转换完成:\n\n${transformationResult.result}` let formattedContent = `[${transformationType}] transformation complete:\n\n${transformationResult.result}`;
// 如果内容被截断,添加提示
if (transformationResult.truncated) { 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 { return {
@ -898,9 +881,9 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
id: uuidv4(), id: uuidv4(),
mentionables: [], mentionables: [],
} }
} };
} catch (error) { } catch (error) {
console.error(`转换失败 (${toolArgs.type}):`, error) console.error(`Transformation failed (${toolArgs.transformation}):`, error);
return { return {
type: toolArgs.type, type: toolArgs.type,
applyMsgId, applyMsgId,
@ -909,11 +892,11 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
role: 'user', role: 'user',
applyStatus: ApplyStatus.Idle, applyStatus: ApplyStatus.Idle,
content: null, 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(), id: uuidv4(),
mentionables: [], mentionables: [],
} }
} };
} }
} }
} catch (error) { } catch (error) {

View File

@ -5,21 +5,19 @@ import { useApp } from "../../../contexts/AppContext"
import { ApplyStatus, ToolArgs } from "../../../types/apply" import { ApplyStatus, ToolArgs } from "../../../types/apply"
import { openMarkdownFile } from "../../../utils/obsidian" 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 { interface MarkdownTransformationToolBlockProps {
applyStatus: ApplyStatus applyStatus: ApplyStatus
onApply: (args: ToolArgs) => void onApply: (args: ToolArgs) => void
toolType: TransformationToolType toolType: TransformationToolType
path: string path: string
depth?: number transformation?: string
format?: string
include_summary?: boolean
finish: boolean finish: boolean
} }
const getToolConfig = (toolType: TransformationToolType) => { const getTransformationConfig = (transformation: string) => {
switch (toolType) { switch (transformation) {
case 'analyze_paper': case 'analyze_paper':
return { return {
icon: <Sparkles size={14} className="infio-chat-code-block-header-icon" />, icon: <Sparkles size={14} className="infio-chat-code-block-header-icon" />,
@ -68,15 +66,12 @@ const getToolConfig = (toolType: TransformationToolType) => {
export default function MarkdownTransformationToolBlock({ export default function MarkdownTransformationToolBlock({
applyStatus, applyStatus,
onApply, onApply,
toolType,
path, path,
depth, transformation,
format,
include_summary,
finish finish
}: MarkdownTransformationToolBlockProps) { }: MarkdownTransformationToolBlockProps) {
const app = useApp() const app = useApp()
const config = getToolConfig(toolType) const config = getTransformationConfig(transformation || '')
const handleClick = () => { const handleClick = () => {
if (path) { if (path) {
@ -86,32 +81,15 @@ export default function MarkdownTransformationToolBlock({
React.useEffect(() => { React.useEffect(() => {
if (finish && applyStatus === ApplyStatus.Idle) { if (finish && applyStatus === ApplyStatus.Idle) {
// 构建符合标准ToolArgs类型的参数 onApply({
if (toolType === 'table_of_contents') { type: 'call_transformations',
onApply({ path: path || '',
type: toolType, transformation: transformation || ''
path: path || '', })
depth,
format,
include_summary
})
} else {
onApply({
type: toolType,
path: path || '',
})
}
} }
}, [finish]) }, [finish])
const getDisplayText = () => { 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 || '未指定路径'}` return `${config.title}: ${path || '未指定路径'}`
} }

View File

@ -213,61 +213,14 @@ function ReactMarkdown({
outputFormat={block.outputFormat} outputFormat={block.outputFormat}
finish={block.finish} finish={block.finish}
/> />
) : block.type === 'analyze_paper' ? ( ) : block.type === 'call_transformations' ? (
<MarkdownTransformationToolBlock <MarkdownTransformationToolBlock
key={"analyze-paper-" + index} key={"call-transformations-" + index}
applyStatus={applyStatus} applyStatus={applyStatus}
onApply={onApply} onApply={onApply}
toolType="analyze_paper" toolType="call_transformations"
path={block.path}
finish={block.finish}
/>
) : block.type === 'key_insights' ? (
<MarkdownTransformationToolBlock
key={"key-insights-" + index}
applyStatus={applyStatus}
onApply={onApply}
toolType="key_insights"
path={block.path}
finish={block.finish}
/>
) : block.type === 'dense_summary' ? (
<MarkdownTransformationToolBlock
key={"dense-summary-" + index}
applyStatus={applyStatus}
onApply={onApply}
toolType="dense_summary"
path={block.path}
finish={block.finish}
/>
) : block.type === 'reflections' ? (
<MarkdownTransformationToolBlock
key={"reflections-" + index}
applyStatus={applyStatus}
onApply={onApply}
toolType="reflections"
path={block.path}
finish={block.finish}
/>
) : block.type === 'table_of_contents' ? (
<MarkdownTransformationToolBlock
key={"table-of-contents-" + index}
applyStatus={applyStatus}
onApply={onApply}
toolType="table_of_contents"
path={block.path}
depth={block.depth}
format={block.format}
include_summary={block.include_summary}
finish={block.finish}
/>
) : block.type === 'simple_summary' ? (
<MarkdownTransformationToolBlock
key={"simple-summary-" + index}
applyStatus={applyStatus}
onApply={onApply}
toolType="simple_summary"
path={block.path} path={block.path}
transformation={block.transformation}
finish={block.finish} finish={block.finish}
/> />
) : block.type === 'tool_result' ? ( ) : block.type === 'tool_result' ? (

View File

@ -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." 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( function getObsidianCapabilitiesSection(
cwd: string, cwd: string,
searchFilesTool: string, searchFilesTool: string,
@ -78,13 +111,16 @@ CAPABILITIES
export function getCapabilitiesSection( export function getCapabilitiesSection(
mode: string, mode: string,
cwd: string, cwd: string,
searchWebTool: string, searchFileTool: string,
): string { ): string {
if (mode === 'ask') {
return getAskModeCapabilitiesSection(cwd, searchFileTool);
}
if (mode === 'research') { if (mode === 'research') {
return getDeepResearchCapabilitiesSection(); return getDeepResearchCapabilitiesSection();
} }
if (mode === 'learn') { if (mode === 'learn') {
return getLearnModeCapabilitiesSection(cwd, searchWebTool); return getLearnModeCapabilitiesSection(cwd, searchFileTool);
} }
return getObsidianCapabilitiesSection(cwd, searchWebTool); return getObsidianCapabilitiesSection(cwd, searchFileTool);
} }

View File

@ -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 <thinking>**: Inside \`<thinking>\` 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 { function getLearnModeObjectiveSection(): string {
return `==== return `====
@ -20,7 +41,7 @@ You enhance learning and comprehension by transforming information into digestib
Before using any tool, analyze the learning context within <thinking></thinking> 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.` Before using any tool, analyze the learning context within <thinking></thinking> 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 `==== return `====
OBJECTIVE 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 { export function getObjectiveSection(mode: string): string {
if (mode === 'ask') {
return getAskModeObjectiveSection();
}
if (mode === 'research') { if (mode === 'research') {
return getDeepResearchObjectiveSection(); return getDeepResearchObjectiveSection();
} }

View File

@ -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 <thinking> 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 <thinking> tags:
<thinking>
**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.]
</thinking>
`
}
function getObsidianRulesSection( function getObsidianRulesSection(
mode: string, mode: string,
cwd: string, cwd: string,
@ -107,6 +137,9 @@ export function getRulesSection(
diffStrategy?: DiffStrategy, diffStrategy?: DiffStrategy,
experiments?: Record<string, boolean> | undefined, experiments?: Record<string, boolean> | undefined,
): string { ): string {
if (mode === 'ask') {
return getAskModeRulesSection(cwd, searchTool);
}
if (mode === 'research') { if (mode === 'research') {
return getDeepResearchRulesSection(); return getDeepResearchRulesSection();
} }

View File

@ -1,4 +1,3 @@
import os from "os"
import { Platform } from 'obsidian'; import { Platform } from 'obsidian';

View File

@ -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 { function getLearnModeToolUseGuidelines(): string {
return `# Tool Use Guidelines 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 { export function getToolUseGuidelinesSection(mode?: string): string {
if (mode === 'ask') {
return getAskModeToolUseGuidelines()
}
if (mode === 'learn') { if (mode === 'learn') {
return getLearnModeToolUseGuidelines() return getLearnModeToolUseGuidelines()
} }

View File

@ -1,4 +1,3 @@
import * as path from 'path' import * as path from 'path'
import { App, normalizePath } from 'obsidian' import { App, normalizePath } from 'obsidian'
@ -10,9 +9,9 @@ import {
ModeConfig, ModeConfig,
PromptComponent, PromptComponent,
defaultModeSlug, defaultModeSlug,
defaultModes,
getGroupName, getGroupName,
getModeBySlug, getModeBySlug
defaultModes
} from "../../utils/modes" } from "../../utils/modes"
import { DiffStrategy } from "../diff/DiffStrategy" import { DiffStrategy } from "../diff/DiffStrategy"
import { McpHub } from "../mcp/McpHub" import { McpHub } from "../mcp/McpHub"
@ -138,7 +137,7 @@ ${getRulesSection(
experiments, experiments,
)} )}
${getSystemInfoSection(cwd)} // ${getSystemInfoSection(cwd)}
${getObjectiveSection(mode)} ${getObjectiveSection(mode)}

View File

@ -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:
<insights>
<path>path/to/your/file.md</path>
<type>simple_summary</type>
</insights>
Example: Getting the key insights from a project note
<insights>
<path>Projects/Project_Alpha_Retrospective.md</path>
<type>key_insights</type>
</insights>`
}

View File

@ -2,12 +2,14 @@ import { ToolArgs } from "./types"
export function getDataviewQueryDescription(args: ToolArgs): string { export function getDataviewQueryDescription(args: ToolArgs): string {
return `## dataview_query 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: Parameters:
- query: (required) The Dataview query statement to execute. Supports DQL (Dataview Query Language) syntax, including TABLE, LIST, TASK query types - query: (required) The Dataview query statement (DQL).
- output_format: (optional) Output format, options: table, list, task, calendar (defaults to table)
Common Query Patterns: 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:** **Time-based Queries:**
- Recently created: \`WHERE file.ctime >= date(today) - dur(7 days)\` - Recently created: \`WHERE file.ctime >= date(today) - dur(7 days)\`
@ -21,7 +23,6 @@ Common Query Patterns:
**Task-based Queries:** **Task-based Queries:**
- Incomplete tasks: \`TASK WHERE !completed\` - Incomplete tasks: \`TASK WHERE !completed\`
- Completed tasks: \`TASK WHERE completed\`
- Specific priority tasks: \`TASK WHERE contains(text, "high priority")\` - Specific priority tasks: \`TASK WHERE contains(text, "high priority")\`
**File Property Queries:** **File Property Queries:**

View File

@ -6,6 +6,7 @@ import { McpHub } from "../../mcp/McpHub"
import { getAccessMcpResourceDescription } from "./access-mcp-resource" import { getAccessMcpResourceDescription } from "./access-mcp-resource"
import { getAskFollowupQuestionDescription } from "./ask-followup-question" import { getAskFollowupQuestionDescription } from "./ask-followup-question"
import { getAttemptCompletionDescription } from "./attempt-completion" import { getAttemptCompletionDescription } from "./attempt-completion"
import { getCallInsightsDescription } from "./call-insights"
import { getDataviewQueryDescription } from "./dataview-query" import { getDataviewQueryDescription } from "./dataview-query"
import { getFetchUrlsContentDescription } from "./fetch-url-content" import { getFetchUrlsContentDescription } from "./fetch-url-content"
import { getInsertContentDescription } from "./insert-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 { ALWAYS_AVAILABLE_TOOLS, TOOL_GROUPS } from "./tool-groups"
import { ToolArgs } from "./types" import { ToolArgs } from "./types"
import { getUseMcpToolDescription } from "./use-mcp-tool" import { getUseMcpToolDescription } from "./use-mcp-tool"
import {
getAnalyzePaperDescription,
getDenseSummaryDescription,
getKeyInsightsDescription,
getReflectionsDescription,
getSimpleSummaryDescription,
getTableOfContentsDescription
} from "./use-transformations-tool"
import { getWriteToFileDescription } from "./write-to-file" import { getWriteToFileDescription } from "./write-to-file"
// Map of tool names to their description functions // Map of tool names to their description functions
@ -34,6 +27,7 @@ const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined>
write_to_file: (args) => getWriteToFileDescription(args), write_to_file: (args) => getWriteToFileDescription(args),
search_files: (args) => getSearchFilesDescription(args), search_files: (args) => getSearchFilesDescription(args),
list_files: (args) => getListFilesDescription(args), list_files: (args) => getListFilesDescription(args),
insights: (args) => getCallInsightsDescription(args),
dataview_query: (args) => getDataviewQueryDescription(args), dataview_query: (args) => getDataviewQueryDescription(args),
ask_followup_question: () => getAskFollowupQuestionDescription(), ask_followup_question: () => getAskFollowupQuestionDescription(),
attempt_completion: () => getAttemptCompletionDescription(), attempt_completion: () => getAttemptCompletionDescription(),
@ -46,12 +40,6 @@ const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined>
args.diffStrategy ? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions }) : "", args.diffStrategy ? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions }) : "",
search_web: (args): string | undefined => getSearchWebDescription(args), search_web: (args): string | undefined => getSearchWebDescription(args),
fetch_urls_content: (args): string | undefined => getFetchUrlsContentDescription(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( export function getToolDescriptionsForMode(
@ -117,11 +105,8 @@ export function getToolDescriptionsForMode(
// Export individual description functions for backward compatibility // Export individual description functions for backward compatibility
export { export {
getAccessMcpResourceDescription, getAnalyzePaperDescription, getAskFollowupQuestionDescription, getAccessMcpResourceDescription, getReadFileDescription, getWriteToFileDescription, getSearchFilesDescription, getListFilesDescription,
getAttemptCompletionDescription, getDataviewQueryDescription, getAskFollowupQuestionDescription, getAttemptCompletionDescription, getSwitchModeDescription, getInsertContentDescription,
getDataviewQueryDescription, getDenseSummaryDescription, getInsertContentDescription, getKeyInsightsDescription, getListFilesDescription, getUseMcpToolDescription, getSearchAndReplaceDescription, getSearchWebDescription, getFetchUrlsContentDescription, getCallInsightsDescription as getCallInsightsDescription
getReadFileDescription, getReflectionsDescription, getSearchAndReplaceDescription,
getSearchFilesDescription, getSimpleSummaryDescription, getSwitchModeDescription, getTableOfContentsDescription, getUseMcpToolDescription,
getWriteToFileDescription
} }

View File

@ -15,15 +15,10 @@ export const TOOL_DISPLAY_NAMES = {
dataview_query: "query dataview", dataview_query: "query dataview",
use_mcp_tool: "use mcp tools", use_mcp_tool: "use mcp tools",
access_mcp_resource: "access mcp resources", access_mcp_resource: "access mcp resources",
insights: "call insights",
ask_followup_question: "ask questions", ask_followup_question: "ask questions",
attempt_completion: "complete tasks", attempt_completion: "complete tasks",
switch_mode: "switch modes", 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 } as const
// Define available tool groups // Define available tool groups
@ -37,8 +32,8 @@ export const TOOL_GROUPS: Record<string, ToolGroupConfig> = {
research: { research: {
tools: ["search_web", "fetch_urls_content"], tools: ["search_web", "fetch_urls_content"],
}, },
transformations: { insights: {
tools: ["analyze_paper", "key_insights", "dense_summary", "reflections", "table_of_contents", "simple_summary"], tools: ["insights"],
}, },
mcp: { mcp: {
tools: ["use_mcp_tool", "access_mcp_resource"], tools: ["use_mcp_tool", "access_mcp_resource"],
@ -75,7 +70,7 @@ export const GROUP_DISPLAY_NAMES: Record<ToolGroup, string> = {
read: "Read Files", read: "Read Files",
edit: "Edit Files", edit: "Edit Files",
research: "Research", research: "Research",
transformations: "Transformations", insights: "insights",
mcp: "MCP Tools", mcp: "MCP Tools",
modes: "Modes", modes: "Modes",
} }

View File

@ -6,6 +6,7 @@ import { createAndInitDb } from '../pgworker'
import { CommandManager } from './modules/command/command-manager' import { CommandManager } from './modules/command/command-manager'
import { ConversationManager } from './modules/conversation/conversation-manager' import { ConversationManager } from './modules/conversation/conversation-manager'
import { InsightManager } from './modules/insight/insight-manager'
import { VectorManager } from './modules/vector/vector-manager' import { VectorManager } from './modules/vector/vector-manager'
export class DBManager { export class DBManager {
@ -14,6 +15,7 @@ export class DBManager {
private vectorManager: VectorManager private vectorManager: VectorManager
private CommandManager: CommandManager private CommandManager: CommandManager
private conversationManager: ConversationManager private conversationManager: ConversationManager
private insightManager: InsightManager
constructor(app: App) { constructor(app: App) {
this.app = app this.app = app
@ -26,6 +28,7 @@ export class DBManager {
dbManager.vectorManager = new VectorManager(app, dbManager) dbManager.vectorManager = new VectorManager(app, dbManager)
dbManager.CommandManager = new CommandManager(app, dbManager) dbManager.CommandManager = new CommandManager(app, dbManager)
dbManager.conversationManager = new ConversationManager(app, dbManager) dbManager.conversationManager = new ConversationManager(app, dbManager)
dbManager.insightManager = new InsightManager(app, dbManager)
return dbManager return dbManager
} }
@ -46,6 +49,10 @@ export class DBManager {
return this.conversationManager return this.conversationManager
} }
getInsightManager(): InsightManager {
return this.insightManager
}
async cleanup() { async cleanup() {
this.db?.close() this.db?.close()
this.db = null this.db = null

View File

@ -0,0 +1,2 @@
export { InsightRepository } from './insight-repository'
export { InsightManager } from './insight-manager'

View File

@ -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<SelectSourceInsight, 'embedding'> & {
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<void> {
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<void> {
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<void> {
const updateData: Partial<InsertSourceInsight> = {}
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<SelectSourceInsight[]> {
return await this.repository.getAllInsights(embeddingModel)
}
/**
*
*/
async getInsightsBySourcePath(
sourcePath: string,
embeddingModel: EmbeddingModel,
): Promise<SelectSourceInsight[]> {
return await this.repository.getInsightsBySourcePath(sourcePath, embeddingModel)
}
/**
*
*/
async getInsightsByType(
insightType: string,
embeddingModel: EmbeddingModel,
): Promise<SelectSourceInsight[]> {
return await this.repository.getInsightsByType(insightType, embeddingModel)
}
/**
*
*/
async getInsightsBySourceType(
sourceType: 'document' | 'tag' | 'folder',
embeddingModel: EmbeddingModel,
): Promise<SelectSourceInsight[]> {
return await this.repository.getInsightsBySourceType(sourceType, embeddingModel)
}
/**
*
*/
async deleteInsightsBySourcePath(
sourcePath: string,
embeddingModel: EmbeddingModel,
): Promise<void> {
await this.repository.deleteInsightsBySourcePath(sourcePath, embeddingModel)
}
/**
*
*/
async deleteInsightsBySourcePaths(
sourcePaths: string[],
embeddingModel: EmbeddingModel,
): Promise<void> {
await this.repository.deleteInsightsBySourcePaths(sourcePaths, embeddingModel)
}
/**
*
*/
async deleteInsightsByType(
insightType: string,
embeddingModel: EmbeddingModel,
): Promise<void> {
await this.repository.deleteInsightsByType(insightType, embeddingModel)
}
/**
*
*/
async clearAllInsights(embeddingModel: EmbeddingModel): Promise<void> {
await this.repository.clearAllInsights(embeddingModel)
}
/**
*
*/
async cleanInsightsForDeletedFile(
file: TFile,
embeddingModel: EmbeddingModel,
): Promise<void> {
await this.repository.deleteInsightsBySourcePath(file.path, embeddingModel)
}
/**
*
*/
async updateInsightsForRenamedFile(
oldPath: string,
newPath: string,
embeddingModel: EmbeddingModel,
): Promise<void> {
// 获取旧路径的所有洞察
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<void> {
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<string, number>
bySourceType: Record<string, number>
}> {
const allInsights = await this.repository.getAllInsights(embeddingModel)
const stats = {
total: allInsights.length,
byType: {} as Record<string, number>,
bySourceType: {} as Record<string, number>,
}
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<SelectSourceInsight[]> {
// 这里可以实现基于文本的搜索逻辑
// 目前先返回所有洞察,然后在内存中过滤
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
}
}

View File

@ -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<SelectSourceInsight[]> {
if (!this.db) {
throw new DatabaseNotInitializedException()
}
const tableName = this.getTableName(embeddingModel)
const result = await this.db.query<SelectSourceInsight>(
`SELECT * FROM "${tableName}" ORDER BY created_at DESC`
)
return result.rows
}
async getInsightsBySourcePath(
sourcePath: string,
embeddingModel: EmbeddingModel,
): Promise<SelectSourceInsight[]> {
if (!this.db) {
throw new DatabaseNotInitializedException()
}
const tableName = this.getTableName(embeddingModel)
const result = await this.db.query<SelectSourceInsight>(
`SELECT * FROM "${tableName}" WHERE source_path = $1 ORDER BY created_at DESC`,
[sourcePath]
)
return result.rows
}
async getInsightsByType(
insightType: string,
embeddingModel: EmbeddingModel,
): Promise<SelectSourceInsight[]> {
if (!this.db) {
throw new DatabaseNotInitializedException()
}
const tableName = this.getTableName(embeddingModel)
const result = await this.db.query<SelectSourceInsight>(
`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<SelectSourceInsight[]> {
if (!this.db) {
throw new DatabaseNotInitializedException()
}
const tableName = this.getTableName(embeddingModel)
const result = await this.db.query<SelectSourceInsight>(
`SELECT * FROM "${tableName}" WHERE source_type = $1 ORDER BY created_at DESC`,
[sourceType]
)
return result.rows
}
async deleteInsightsBySourcePath(
sourcePath: string,
embeddingModel: EmbeddingModel,
): Promise<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<InsertSourceInsight>,
embeddingModel: EmbeddingModel,
): Promise<void> {
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<SelectSourceInsight, 'embedding'> & {
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<SelectSourceInsight, 'embedding'> & { similarity: number }
const result = await this.db.query<SearchResult>(query, params)
return result.rows
}
}

View File

@ -136,7 +136,7 @@ export class VectorRepository {
const tableName = this.getTableName(embeddingModel) const tableName = this.getTableName(embeddingModel)
let scopeCondition = '' let scopeCondition = ''
const params: any[] = [`[${queryVector.join(',')}]`, options.minSimilarity, options.limit] const params: unknown[] = [`[${queryVector.join(',')}]`, options.minSimilarity, options.limit]
let paramIndex = 4 let paramIndex = 4
if (options.scope) { if (options.scope) {

View File

@ -176,3 +176,63 @@ export type SelectMessage = {
similarity_search_results?: string | null similarity_search_results?: string | null
created_at: Date 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<SourceInsightRecord, 'id' | 'created_at' | 'updated_at'>
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<number, TableDefinition>
>((acc, dimension) => {
acc[dimension] = createSourceInsightTable(dimension)
return acc
}, {})

View File

@ -94,6 +94,119 @@ export const migrations: Record<string, SqlMigration> = {
ON "embeddings_384" ("path"); 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: { template: {
description: "Creates template table with UUID support", description: "Creates template table with UUID support",
sql: ` sql: `

View File

@ -112,43 +112,11 @@ export type DataviewQueryToolArgs = {
finish?: boolean; finish?: boolean;
} }
export type AnalyzePaperToolArgs = { export type CallTransformationsToolArgs = {
type: 'analyze_paper'; type: 'call_transformations';
path: string; path: string;
transformation: string;
finish?: boolean; finish?: boolean;
} }
export type KeyInsightsToolArgs = { export type ToolArgs = ReadFileToolArgs | WriteToFileToolArgs | InsertContentToolArgs | SearchAndReplaceToolArgs | ListFilesToolArgs | MatchSearchFilesToolArgs | RegexSearchFilesToolArgs | SemanticSearchFilesToolArgs | SearchWebToolArgs | FetchUrlsContentToolArgs | SwitchModeToolArgs | ApplyDiffToolArgs | UseMcpToolArgs | DataviewQueryToolArgs | CallTransformationsToolArgs;
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;

View File

@ -88,8 +88,8 @@ export const defaultModes: ModeConfig[] = [
slug: "ask", slug: "ask",
name: "Ask", name: "Ask",
roleDefinition: roleDefinition:
"You are Infio, an AI knowledge assistant powered by advanced language models. You operate within Obsidian.", "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", "transformations", "mcp"], groups: ["read", "insights", "mcp"],
customInstructions: 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.", "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", name: "Learn",
roleDefinition: roleDefinition:
"You are Infio, an AI learning assistant powered by advanced language models. You operate within Obsidian.", "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: 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." "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."
}, },

View File

@ -98,36 +98,14 @@ export type ParsedMsgBlock =
query: string query: string
outputFormat: string outputFormat: string
finish: boolean finish: boolean
} | {
type: 'call_transformations'
path: string
transformation: string
finish: boolean
} | { } | {
type: 'tool_result' type: 'tool_result'
content: string 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( export function parseMsgBlocks(
@ -739,7 +717,7 @@ export function parseMsgBlocks(
finish: node.sourceCodeLocation.endTag !== undefined finish: node.sourceCodeLocation.endTag !== undefined
}) })
lastEndOffset = endOffset lastEndOffset = endOffset
} else if (node.nodeName === 'analyze_paper') { } else if (node.nodeName === 'insights') {
if (!node.sourceCodeLocation) { if (!node.sourceCodeLocation) {
throw new Error('sourceCodeLocation is undefined') throw new Error('sourceCodeLocation is undefined')
} }
@ -752,159 +730,22 @@ export function parseMsgBlocks(
}) })
} }
let path: string | undefined let path: string | undefined
let transformation: string | undefined
for (const childNode of node.childNodes) { for (const childNode of node.childNodes) {
if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) { if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) {
// @ts-expect-error - parse5 node value type // @ts-expect-error - parse5 node value type
path = childNode.childNodes[0].value 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({ parsedResult.push({
type: 'analyze_paper', type: 'call_transformations',
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',
path: path || '', path: path || '',
transformation: transformation || '',
finish: node.sourceCodeLocation.endTag !== undefined finish: node.sourceCodeLocation.endTag !== undefined
}) })
lastEndOffset = endOffset lastEndOffset = endOffset

View File

@ -8,6 +8,7 @@ import { SystemPrompt } from '../core/prompts/system'
import { RAGEngine } from '../core/rag/rag-engine' import { RAGEngine } from '../core/rag/rag-engine'
import { ConvertDataManager } from '../database/json/convert-data/ConvertDataManager' import { ConvertDataManager } from '../database/json/convert-data/ConvertDataManager'
import { ConvertType } from '../database/json/convert-data/types' import { ConvertType } from '../database/json/convert-data/types'
import { WorkspaceManager } from '../database/json/workspace/WorkspaceManager'
import { SelectVector } from '../database/schema' import { SelectVector } from '../database/schema'
import { ChatMessage, ChatUserMessage } from '../types/chat' import { ChatMessage, ChatUserMessage } from '../types/chat'
import { ContentPart, RequestMessage } from '../types/llm/request' 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 { export class PromptGenerator {
private getRagEngine: () => Promise<RAGEngine> private getRagEngine: () => Promise<RAGEngine>
private app: App private app: App
@ -144,6 +152,7 @@ export class PromptGenerator {
private customModeList: ModeConfig[] | null = null private customModeList: ModeConfig[] | null = null
private getMcpHub: () => Promise<McpHub> | null = null private getMcpHub: () => Promise<McpHub> | null = null
private convertDataManager: ConvertDataManager private convertDataManager: ConvertDataManager
private workspaceManager: WorkspaceManager
private static readonly EMPTY_ASSISTANT_MESSAGE: RequestMessage = { private static readonly EMPTY_ASSISTANT_MESSAGE: RequestMessage = {
role: 'assistant', role: 'assistant',
content: '', content: '',
@ -167,6 +176,7 @@ export class PromptGenerator {
this.customModeList = customModeList ?? null this.customModeList = customModeList ?? null
this.getMcpHub = getMcpHub ?? null this.getMcpHub = getMcpHub ?? null
this.convertDataManager = new ConvertDataManager(app) this.convertDataManager = new ConvertDataManager(app)
this.workspaceManager = new WorkspaceManager(app)
} }
public async generateRequestMessages({ public async generateRequestMessages({
@ -243,72 +253,154 @@ export class PromptGenerator {
} }
} }
private async getEnvironmentDetails() { private async getEnvironmentDetails(): Promise<string> {
let details = "" const currentNoteContext = await this.getCurrentNoteContext()
// Obsidian Current File const noteConnectivity = await this.getNoteConnectivity()
details += "\n\n# Obsidian Current File" const workspaceOverview = await this.getWorkspaceOverview()
const currentFile = this.app.workspace.getActiveFile() const assistantState = await this.getAssistantState()
if (currentFile) {
details += `\n${currentFile?.path}` const details = [
} else { currentNoteContext,
details += "\n(No current file)" 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 += `<slug>${currentMode}</slug>\n`
details += `<name>${modeDetails.name}</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 `<environment_details>\n${details.trim()}\n</environment_details>` return `<environment_details>\n${details.trim()}\n</environment_details>`
} }
private async getCurrentNoteContext(): Promise<string | null> {
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<string | null> {
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<string> {
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<string> {
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 = `<slug>${currentMode}</slug>\n<name>${modeDetails.name}</name>`
state += `\n\n## Current Mode\n${modeInfo}`
return formatSection('Assistant & User State', state.trim())
}
private async compileUserMessagePrompt({ private async compileUserMessagePrompt({
isNewChat, isNewChat,
message, message,

View File

@ -37,8 +37,8 @@ export const TOOL_GROUPS: Record<string, ToolGroupConfig> = {
research: { research: {
tools: ["search_web", "fetch_urls_content"], tools: ["search_web", "fetch_urls_content"],
}, },
transformations: { insights: {
tools: ["analyze_paper", "key_insights", "dense_summary", "reflections", "table_of_contents", "simple_summary"], tools: ["insights"],
}, },
mcp: { mcp: {
tools: ["use_mcp_tool", "access_mcp_resource"], tools: ["use_mcp_tool", "access_mcp_resource"],