Skip to content

Commit ffb4b39

Browse files
authored
chore:Auto correct context; support extract image paths from selections (#27)
1 parent b563002 commit ffb4b39

17 files changed

+157
-47
lines changed

src/constants.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,43 @@ export const STW_EMBEDDED_CONVERSATION_VIEW_CONFIG = {
1212
icon: 'message-square',
1313
};
1414

15-
// Pattern only, without flags, to avoid global regex state issues
16-
// Captures image path (group 1) which may be followed by size parameter after |
15+
/**
16+
* Pattern only, without flags, to avoid global regex state issues
17+
* Captures image path (group 1) which may be followed by size parameter after |
18+
*/
1719
export const IMAGE_LINK_PATTERN = '!\\[\\[(.*?\\.(jpg|jpeg|png|webp|svg))(?:\\|.*?)?\\]\\]';
18-
// Stw-selected pattern constants for reuse across the application
19-
// Pattern to match any stw-selected block (with capture group for splitting)
20+
21+
/**
22+
* Stw-selected pattern constants for reuse across the application
23+
* Pattern to match any stw-selected block (with capture group for splitting)
24+
*/
2025
export const STW_SELECTED_PATTERN = '(\\{\\{stw-selected.*?\\}\\})';
2126

22-
// Pattern to match {{stw-squeezed [[<path>]] }}
27+
/**
28+
* The placeholder for the stw-selected blocks in the original query
29+
* This helps to reduce the complexity for the planner to just put the placeholder rather than extract the stw-selected blocks
30+
*/
31+
export const STW_SELECTED_PLACEHOLDER = '<stwSelected>';
32+
33+
/**
34+
* Pattern to match {{stw-squeezed [[<path>]] }}
35+
*/
2336
export const STW_SQUEEZED_PATTERN = '\\{\\{stw-squeezed \\[\\[([^\\]]+)\\]\\] \\}\\}';
2437

25-
// Pattern to match any wikilink
38+
/**
39+
* Pattern to match any wikilink
40+
*/
2641
export const WIKI_LINK_PATTERN = '\\[\\[([^\\]]+)\\]\\]';
2742

28-
// Pattern to extract metadata from stw-selected blocks
43+
/**
44+
* Pattern to extract metadata from stw-selected blocks
45+
*/
2946
export const STW_SELECTED_METADATA_PATTERN =
3047
'\\{\\{stw-selected from:(\\d+),to:(\\d+),selection:(.+?),path:(.+?)\\}\\}';
3148

32-
// Supported command prefixes
49+
/**
50+
* Supported command prefixes
51+
*/
3352
export const COMMAND_PREFIXES = [
3453
'/ ',
3554
'/search',

src/lib/modelfusion/extractions/contentUpdateExtraction.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { contentUpdatePrompt } from '../prompts/contentUpdatePrompt';
33
import { userLanguagePrompt } from '../prompts/languagePrompt';
44
import { logger } from 'src/utils/logger';
55
import { AbortService } from 'src/services/AbortService';
6-
import { prepareUserMessage } from '../utils/userMessageUtils';
6+
import { prepareMessage } from '../utils/messageUtils';
77
import { App } from 'obsidian';
88
import { LLMService } from 'src/services/LLMService';
99
import { z } from 'zod';
@@ -64,7 +64,7 @@ export async function extractContentUpdate(params: {
6464
generateType: 'object',
6565
});
6666

67-
const userMessage = await prepareUserMessage(command.query, app);
67+
const userMessage = await prepareMessage(command.query, app);
6868

6969
const { object } = await generateObject({
7070
...llmConfig,

src/lib/modelfusion/extractions/noteCreationExtraction.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { generateObject } from 'ai';
22
import { noteCreationPrompt } from '../prompts/noteCreationPrompt';
33
import { userLanguagePrompt } from '../prompts/languagePrompt';
44
import { AbortService } from 'src/services/AbortService';
5-
import { prepareUserMessage } from '../utils/userMessageUtils';
65
import { App } from 'obsidian';
76
import { LLMService } from 'src/services/LLMService';
87
import { z } from 'zod';
98
import { CommandIntent } from 'src/types/types';
109
import { explanationFragment, confidenceFragment } from '../prompts/fragments';
1110
import { logger } from 'src/utils/logger';
11+
import { prepareMessage } from '../utils/messageUtils';
1212

1313
const abortService = AbortService.getInstance();
1414

@@ -56,7 +56,7 @@ export async function extractNoteCreation(params: {
5656
});
5757

5858
// Prepare user message with potential image content
59-
const userMessage = await prepareUserMessage(command.query, app);
59+
const userMessage = await prepareMessage(command.query, app);
6060

6161
const { object } = await generateObject({
6262
...llmConfig,

src/lib/modelfusion/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ export * from './validators';
88
export { getClassifier } from './classifiers/getClassifier';
99

1010
// Export image utils
11-
export { prepareUserMessage } from './utils/userMessageUtils';
11+
export { prepareMessage } from './utils/messageUtils';

src/lib/modelfusion/prompts/__snapshots__/commands.test.ts.snap

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,20 @@ exports[`commands formatCommandsForPrompt should return formatted commands for p
2626
Use when: Delete notes from the artifact
2727
- "build_search_index" (aliases: index, build-index, search-index): Build or rebuild the search index for all markdown files in the vault
2828
Use when: Build or rebuild the search index for all markdown files in the vault
29-
- "generate": Generate content with the LLM help (either in a new note or in the conversation). You also can "generate" from the provided content in the user's query without reading the note. Example: "Help me update this list to the numbered list:\\n- Item 1\\n- Item 2" -> ["generate"]. The list is already in the query.
29+
- "generate": Generate content with the LLM help (either in a new note or in the conversation). You also can "generate" from the provided content in the user's query without reading the note. Example: "Help me update this list to the numbered list:
30+
- Item 1
31+
- Item 2" -> ["generate"]. The list is already in the query.
3032
Use when: Ask or generate content with your help
3133
- "read": Read content from the current note or specific position: "above", "below". Use this when you don't know the content and need to retrieve it before proceeding
34+
Can read any content type, including code blocks, tables, lists, paragraphs, images, and more.
3235
Use when: Read or Find content based on a specific pattern in their current note"
3336
`;
3437

3538
exports[`commands formatCommandsForPrompt should return formatted commands for prompt with given command names 1`] = `
3639
"- "search": Find files using the search engine to search files locally and store the result as an artifact
3740
Use when: Search for files (and doesn't mention existing search results)
3841
- "read": Read content from the current note or specific position: "above", "below". Use this when you don't know the content and need to retrieve it before proceeding
42+
Can read any content type, including code blocks, tables, lists, paragraphs, images, and more.
3943
Use when: Read or Find content based on a specific pattern in their current note"
4044
`;
4145

src/lib/modelfusion/prompts/commands.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,9 @@ export const COMMAND_DEFINITIONS: CommandDefinition[] = [
181181
},
182182
{
183183
commandType: 'generate',
184-
description:
185-
'Generate content with the LLM help (either in a new note or in the conversation). You also can "generate" from the provided content in the user\'s query without reading the note. Example: "Help me update this list to the numbered list:\\n- Item 1\\n- Item 2" -> ["generate"]. The list is already in the query.',
184+
description: `Generate content with the LLM help (either in a new note or in the conversation). You also can "generate" from the provided content in the user's query without reading the note. Example: "Help me update this list to the numbered list:
185+
- Item 1
186+
- Item 2" -> ["generate"]. The list is already in the query.`,
186187
category: 'intent-based',
187188
queryTemplate: `Extract the query for the generate command follows this format: <query_in_natural_language>; [note name: <noteName>]
188189
- <query_in_natural_language>: Tailored query for the generate command.
@@ -192,8 +193,8 @@ export const COMMAND_DEFINITIONS: CommandDefinition[] = [
192193
},
193194
{
194195
commandType: 'read',
195-
description:
196-
'Read content from the current note or specific position: "above", "below". Use this when you don\'t know the content and need to retrieve it before proceeding',
196+
description: `Read content from the current note or specific position: "above", "below". Use this when you don't know the content and need to retrieve it before proceeding
197+
Can read any content type, including code blocks, tables, lists, paragraphs, images, and more.`,
197198
category: 'intent-based',
198199
queryTemplate: `Extract a specific query for a read command:
199200
1. Extract the query for the read command follows this format: <query_in_natural_language>; read type: <readType>[; note name: <noteName>]

src/lib/modelfusion/utils/userMessageUtils.ts renamed to src/lib/modelfusion/utils/messageUtils.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,28 @@ import { NoteContentService } from 'src/services/NoteContentService';
66
import { resizeImageWithCanvas } from 'src/utils/resizeImageWithCanvas';
77
import { logger } from 'src/utils/logger';
88

9-
export function getTextContentWithoutImages(userInput: string): string {
9+
export function getTextContentWithoutImages(input: string): string {
1010
// Create a new RegExp instance with flags each time to avoid stateful issues
1111
const imageRegex = new RegExp(IMAGE_LINK_PATTERN, 'gi');
12-
return userInput.replace(imageRegex, '').trim();
12+
return input.replace(imageRegex, '').trim();
1313
}
1414

1515
/**
16-
* Prepares user message content with images and wikilinks's content for OpenAI's Vision API
17-
* @param userInput Original user input text
16+
* Prepares message content with images and wikilinks's content
17+
* @param input Original input text
1818
* @param app Obsidian App instance for accessing vault
19-
* @returns An array of content items for OpenAI's ChatMessage.user
2019
*/
21-
export async function prepareUserMessage(
22-
userInput: string,
20+
export async function prepareMessage(
21+
input: string,
2322
app: App
2423
): Promise<Array<TextPart | ImagePart>> {
2524
const noteContentService = NoteContentService.getInstance(app);
26-
const imagePaths = noteContentService.extractImageLinks(userInput);
27-
const wikilinks = noteContentService.extractWikilinks(userInput);
25+
const imagePaths = noteContentService.extractImageLinks(input);
26+
const wikilinks = noteContentService.extractWikilinks(input);
2827
const messageContent: Array<TextPart | ImagePart> = [];
2928

3029
// Add the original user input first
31-
messageContent.push({ type: 'text', text: userInput });
30+
messageContent.push({ type: 'text', text: input });
3231

3332
const mediaTools = MediaTools.getInstance(app);
3433

src/services/CommandProcessorService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ import {
2424
SummaryCommandHandler,
2525
ContextAugmentationHandler,
2626
} from '../solutions/commands/handlers';
27-
import { getTextContentWithoutImages } from 'src/lib/modelfusion/utils/userMessageUtils';
2827

2928
import type StewardPlugin from '../main';
29+
import { getTextContentWithoutImages } from 'src/lib/modelfusion/utils/messageUtils';
3030

3131
export class CommandProcessorService {
3232
private readonly commandProcessor: CommandProcessor;
@@ -103,7 +103,7 @@ export class CommandProcessorService {
103103
this.commandProcessor.registerHandler('read', readHandler);
104104

105105
// Register the generate command handler
106-
const generateHandler = new GenerateCommandHandler(this.plugin);
106+
const generateHandler = new GenerateCommandHandler(this.plugin, this.commandProcessor);
107107
this.commandProcessor.registerHandler('generate', generateHandler);
108108

109109
// Register the stop command handler

src/services/NoteContentService.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { MarkdownUtil } from 'src/utils/markdownUtils';
12
import { NoteContentService } from './NoteContentService';
23
import { App, TFile } from 'obsidian';
34

@@ -53,6 +54,13 @@ Is the image above a lake, pond, reservoir, or sea?`;
5354
const imageLinks = noteContentService.extractImageLinks(content);
5455
expect(imageLinks).toEqual(['Pasted image 20250222171626.png']);
5556
});
57+
58+
it('should extract images from stw-selected blocks', () => {
59+
const content = `Here is a selected block with an image:
60+
{{stw-selected from:1,to:5,selection: ${new MarkdownUtil('This contains an image: ![[image2.jpg]] and some text').escape().getText()},path:test.md}}`;
61+
const imageLinks = noteContentService.extractImageLinks(content);
62+
expect(imageLinks).toEqual(['image2.jpg']);
63+
});
5664
});
5765

5866
describe('extractWikilinks', () => {

src/services/NoteContentService.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
import { IMAGE_LINK_PATTERN, WIKI_LINK_PATTERN } from 'src/constants';
1+
import {
2+
IMAGE_LINK_PATTERN,
3+
WIKI_LINK_PATTERN,
4+
STW_SELECTED_PATTERN,
5+
STW_SELECTED_METADATA_PATTERN,
6+
} from 'src/constants';
27
import { App } from 'obsidian';
38
import { logger } from 'src/utils/logger';
9+
import { MarkdownUtil } from 'src/utils/markdownUtils';
410

511
export class NoteContentService {
612
private static instance: NoteContentService;
@@ -59,17 +65,44 @@ export class NoteContentService {
5965
* @returns Array of image paths extracted from the content
6066
*/
6167
public extractImageLinks(content: string): string[] {
62-
// Create a new RegExp instance with flags each time to avoid stateful issues
68+
const imagePaths: string[] = [];
69+
70+
// Extract images from regular image links
6371
const imageRegex = new RegExp(IMAGE_LINK_PATTERN, 'gi');
6472
const matches = content.matchAll(imageRegex);
65-
const imagePaths: string[] = [];
6673

6774
for (const match of matches) {
6875
if (match[1]) {
6976
imagePaths.push(match[1]);
7077
}
7178
}
7279

80+
// Early check if content has stw-selected blocks
81+
if (!content.includes('{{stw-selected')) {
82+
return imagePaths;
83+
}
84+
85+
// Extract images from stw-selected blocks
86+
const stwSelectedMatches = content.matchAll(new RegExp(STW_SELECTED_PATTERN, 'g'));
87+
88+
for (const stwMatch of stwSelectedMatches) {
89+
if (stwMatch[1]) {
90+
const stwBlock = stwMatch[1];
91+
const metadataMatch = stwBlock.match(new RegExp(STW_SELECTED_METADATA_PATTERN));
92+
93+
if (metadataMatch) {
94+
const [, , , escapedSelection] = metadataMatch;
95+
// Unescape the selection content
96+
const unescapedSelection = new MarkdownUtil(escapedSelection)
97+
.unescape()
98+
.decodeURI()
99+
.getText();
100+
const selectionImagePaths = this.extractImageLinks(unescapedSelection);
101+
imagePaths.push(...selectionImagePaths);
102+
}
103+
}
104+
}
105+
73106
return imagePaths;
74107
}
75108

0 commit comments

Comments
 (0)