Compact tools
This commit is contained in:
parent
ad8895430b
commit
97f88a24b9
|
|
@ -27,8 +27,19 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
font-size: 1.4em;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--yellow, #e6db74);
|
color: var(--yellow, #e6db74);
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header1 {
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header2 {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header3 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
import styles from './assets/highlight.module.css';
|
import styles from './assets/highlight.module.css';
|
||||||
|
|
||||||
export const highlight = (message: string, keepMarkup = true): string => {
|
export const highlight = (message: string, keepMarkup = true): string => {
|
||||||
let resultHTML = '';
|
let resultHTML = '';
|
||||||
const tokenRegex = /(\*\*?|"|```|`|(?:^|\n)# |\n)/g;
|
const tokenRegex = /(\*\*?|"|```|`|(?:^|\n)#{1,3} |\n)/g;
|
||||||
|
const headerRegex = /#{1,3} $/;
|
||||||
const stack: string[] = [];
|
const stack: string[] = [];
|
||||||
let inCodeBlock = false;
|
let inCodeBlock = false;
|
||||||
let inHeader = false;
|
let inHeader = false;
|
||||||
|
|
@ -28,10 +30,13 @@ export const highlight = (message: string, keepMarkup = true): string => {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token.endsWith('# ')) {
|
const headerMatch = token.match(headerRegex);
|
||||||
|
if (headerMatch) {
|
||||||
if (inHeader) resultHTML += '</span>';
|
if (inHeader) resultHTML += '</span>';
|
||||||
|
const markup = keepMarkup ? headerMatch[0] : '';
|
||||||
|
const len = headerMatch[0].length;
|
||||||
inHeader = true;
|
inHeader = true;
|
||||||
resultHTML += `${token.slice(0, -2)}<span class="${styles.header}">${keepMarkup ? '# ' : ''}`;
|
resultHTML += `${token.slice(0, -len)}<span class="${clsx(styles.header, styles[`header${len - 1}`])}">${markup}`;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ namespace Prompt {
|
||||||
|
|
||||||
const content = slot.mode === 'omitted' ? '[...]'
|
const content = slot.mode === 'omitted' ? '[...]'
|
||||||
: slot.mode === 'summary' ? `[Summary: ${slot.summary}]`
|
: slot.mode === 'summary' ? `[Summary: ${slot.summary}]`
|
||||||
: slot.body;
|
: slot.body;
|
||||||
lines.push(content);
|
lines.push(content);
|
||||||
parts.push(lines.join('\n\n'));
|
parts.push(lines.join('\n\n'));
|
||||||
}
|
}
|
||||||
|
|
@ -172,10 +172,10 @@ namespace Prompt {
|
||||||
|
|
||||||
const parts: string[] = [state.systemInstruction];
|
const parts: string[] = [state.systemInstruction];
|
||||||
|
|
||||||
parts.push(`# ${currentStory.title}`);
|
parts.push(`# Story Title: ${currentStory.title}`);
|
||||||
|
|
||||||
if (currentStory.lore) {
|
if (currentStory.lore) {
|
||||||
parts.push(`## Lore\n${currentStory.lore}`);
|
parts.push(`## Lore`, currentStory.lore);
|
||||||
}
|
}
|
||||||
|
|
||||||
const charactersSection = formatCharactersMarkdown(state);
|
const charactersSection = formatCharactersMarkdown(state);
|
||||||
|
|
@ -191,11 +191,11 @@ namespace Prompt {
|
||||||
if (currentStory.text && storyTokenBudget > 0) {
|
if (currentStory.text && storyTokenBudget > 0) {
|
||||||
const storyText = formatStoryChunks(currentStory.text, currentStory.chapters ?? [], storyTokenBudget);
|
const storyText = formatStoryChunks(currentStory.text, currentStory.chapters ?? [], storyTokenBudget);
|
||||||
if (storyText) {
|
if (storyText) {
|
||||||
parts.push(`## Story\n${storyText}`);
|
parts.push(`## Story Text:`, storyText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts.join('\n\n');
|
return parts.join('\n\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function compilePrompt(state: AppState, newMessages: LLM.ChatMessage[] = []): LLM.ChatCompletionRequest | null {
|
export function compilePrompt(state: AppState, newMessages: LLM.ChatMessage[] = []): LLM.ChatCompletionRequest | null {
|
||||||
|
|
|
||||||
|
|
@ -20,50 +20,6 @@ export namespace Tools {
|
||||||
const tool = <T extends TObject = TObject>(t: Tool<T>): Tool<T> => t;
|
const tool = <T extends TObject = TObject>(t: Tool<T>): Tool<T> => t;
|
||||||
|
|
||||||
const TOOLS: Record<string, Tool> = {
|
const TOOLS: Record<string, Tool> = {
|
||||||
'append_to_story': tool({
|
|
||||||
handler: async (args, appState) => {
|
|
||||||
if (!appState.currentStory) {
|
|
||||||
return 'Error: No story selected';
|
|
||||||
}
|
|
||||||
appState.dispatch({
|
|
||||||
type: 'EDIT_STORY',
|
|
||||||
id: appState.currentStory.id,
|
|
||||||
text: appState.currentStory.text + '\n' + args.text,
|
|
||||||
});
|
|
||||||
appState.dispatch({
|
|
||||||
type: 'SET_CURRENT_TAB',
|
|
||||||
id: appState.currentStory.id,
|
|
||||||
tab: 'story'
|
|
||||||
});
|
|
||||||
return 'Text appended successfully';
|
|
||||||
},
|
|
||||||
description: 'Append text to the current story',
|
|
||||||
parameters: Type.Object({
|
|
||||||
text: Type.String({ description: 'The text to append to the story' }),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
'append_to_lore': tool({
|
|
||||||
handler: async (args, appState) => {
|
|
||||||
if (!appState.currentStory) {
|
|
||||||
return 'Error: No story selected';
|
|
||||||
}
|
|
||||||
appState.dispatch({
|
|
||||||
type: 'EDIT_LORE',
|
|
||||||
id: appState.currentStory.id,
|
|
||||||
lore: appState.currentStory.lore + '\n' + args.text,
|
|
||||||
});
|
|
||||||
appState.dispatch({
|
|
||||||
type: 'SET_CURRENT_TAB',
|
|
||||||
id: appState.currentStory.id,
|
|
||||||
tab: 'lore'
|
|
||||||
});
|
|
||||||
return 'Text appended to lore successfully';
|
|
||||||
},
|
|
||||||
description: 'Append text to the story lore',
|
|
||||||
parameters: Type.Object({
|
|
||||||
text: Type.String({ description: 'The text to append to the lore' }),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
'add_character': tool({
|
'add_character': tool({
|
||||||
handler: async (args, appState) => {
|
handler: async (args, appState) => {
|
||||||
if (!appState.currentStory) {
|
if (!appState.currentStory) {
|
||||||
|
|
@ -229,66 +185,87 @@ export namespace Tools {
|
||||||
})),
|
})),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'edit_story': tool({
|
'edit_text': tool({
|
||||||
handler: async (args, appState) => {
|
handler: async (args, appState) => {
|
||||||
if (!appState.currentStory) {
|
if (!appState.currentStory) {
|
||||||
return 'Error: No story selected';
|
return 'Error: No story selected';
|
||||||
}
|
}
|
||||||
const occurrences = appState.currentStory.text.split(args.old_text).length - 1;
|
const target = args.target ?? 'story';
|
||||||
|
|
||||||
|
// Append mode: when old_text is not provided, append new_text to the target
|
||||||
|
if (args.old_text == null) {
|
||||||
|
if (target === 'lore') {
|
||||||
|
appState.dispatch({
|
||||||
|
type: 'EDIT_LORE',
|
||||||
|
id: appState.currentStory.id,
|
||||||
|
lore: appState.currentStory.lore + '\n' + args.new_text,
|
||||||
|
});
|
||||||
|
appState.dispatch({
|
||||||
|
type: 'SET_CURRENT_TAB',
|
||||||
|
id: appState.currentStory.id,
|
||||||
|
tab: 'lore'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
appState.dispatch({
|
||||||
|
type: 'EDIT_STORY',
|
||||||
|
id: appState.currentStory.id,
|
||||||
|
text: appState.currentStory.text + '\n' + args.new_text,
|
||||||
|
});
|
||||||
|
appState.dispatch({
|
||||||
|
type: 'SET_CURRENT_TAB',
|
||||||
|
id: appState.currentStory.id,
|
||||||
|
tab: 'story'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return `Text appended to ${target} successfully`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace mode: find and replace old_text with new_text
|
||||||
|
const source = target === 'lore'
|
||||||
|
? appState.currentStory.lore
|
||||||
|
: appState.currentStory.text;
|
||||||
|
|
||||||
|
const occurrences = source.split(args.old_text).length - 1;
|
||||||
if (occurrences === 0) {
|
if (occurrences === 0) {
|
||||||
return 'Error: old_text not found in story';
|
return `Error: old_text not found in ${target}`;
|
||||||
}
|
}
|
||||||
if (occurrences > 1 && !args.replace_all) {
|
if (occurrences > 1 && !args.replace_all) {
|
||||||
return 'Error: old_text appears multiple times in story';
|
return `Error: old_text appears multiple times in ${target}`;
|
||||||
}
|
}
|
||||||
appState.dispatch({
|
|
||||||
type: 'EDIT_STORY',
|
if (target === 'lore') {
|
||||||
id: appState.currentStory.id,
|
appState.dispatch({
|
||||||
text: appState.currentStory.text.replaceAll(args.old_text, args.new_text),
|
type: 'EDIT_LORE',
|
||||||
});
|
id: appState.currentStory.id,
|
||||||
appState.dispatch({
|
lore: appState.currentStory.lore.replaceAll(args.old_text, args.new_text),
|
||||||
type: 'SET_CURRENT_TAB',
|
});
|
||||||
id: appState.currentStory.id,
|
appState.dispatch({
|
||||||
tab: 'story'
|
type: 'SET_CURRENT_TAB',
|
||||||
});
|
id: appState.currentStory.id,
|
||||||
return 'Story edited successfully';
|
tab: 'lore'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
appState.dispatch({
|
||||||
|
type: 'EDIT_STORY',
|
||||||
|
id: appState.currentStory.id,
|
||||||
|
text: appState.currentStory.text.replaceAll(args.old_text, args.new_text),
|
||||||
|
});
|
||||||
|
appState.dispatch({
|
||||||
|
type: 'SET_CURRENT_TAB',
|
||||||
|
id: appState.currentStory.id,
|
||||||
|
tab: 'story'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return `${target === 'lore' ? 'Lore' : 'Story'} edited successfully`;
|
||||||
},
|
},
|
||||||
description: 'Replace text in the current story',
|
description: 'Replace text in the story or lore. When old_text is omitted, appends new_text to the target.',
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
old_text: Type.String({ description: 'The text to find and replace in the story' }),
|
new_text: Type.String({ description: 'The new text to replace old_text with, or to append if old_text is omitted' }),
|
||||||
new_text: Type.String({ description: 'The new text to replace old_text with' }),
|
old_text: Type.Optional(Type.String({ description: 'The text to find and replace. If omitted, new_text will be appended' })),
|
||||||
replace_all: Type.Optional(Type.Boolean({ description: 'If true, replace all occurrences of old_text' })),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
'edit_lore': tool({
|
|
||||||
handler: async (args, appState) => {
|
|
||||||
if (!appState.currentStory) {
|
|
||||||
return 'Error: No story selected';
|
|
||||||
}
|
|
||||||
const occurrences = appState.currentStory.lore.split(args.old_text).length - 1;
|
|
||||||
if (occurrences === 0) {
|
|
||||||
return 'Error: old_text not found in lore';
|
|
||||||
}
|
|
||||||
if (occurrences > 1 && !args.replace_all) {
|
|
||||||
return 'Error: old_text appears multiple times in lore';
|
|
||||||
}
|
|
||||||
appState.dispatch({
|
|
||||||
type: 'EDIT_LORE',
|
|
||||||
id: appState.currentStory.id,
|
|
||||||
lore: appState.currentStory.lore.replaceAll(args.old_text, args.new_text),
|
|
||||||
});
|
|
||||||
appState.dispatch({
|
|
||||||
type: 'SET_CURRENT_TAB',
|
|
||||||
id: appState.currentStory.id,
|
|
||||||
tab: 'lore'
|
|
||||||
});
|
|
||||||
return 'Lore edited successfully';
|
|
||||||
},
|
|
||||||
description: 'Replace text in the story lore',
|
|
||||||
parameters: Type.Object({
|
|
||||||
old_text: Type.String({ description: 'The text to find and replace in the lore' }),
|
|
||||||
new_text: Type.String({ description: 'The new text to replace old_text with' }),
|
|
||||||
replace_all: Type.Optional(Type.Boolean({ description: 'If true, replace all occurrences of old_text' })),
|
replace_all: Type.Optional(Type.Boolean({ description: 'If true, replace all occurrences of old_text' })),
|
||||||
|
target: Type.Optional(Type.Enum(['lore', 'story'],
|
||||||
|
{ description: 'Target to edit (story or lore, default: story)' },
|
||||||
|
)),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'grep': tool({
|
'grep': tool({
|
||||||
|
|
@ -296,39 +273,56 @@ export namespace Tools {
|
||||||
if (!appState.currentStory) {
|
if (!appState.currentStory) {
|
||||||
return 'Error: No story selected';
|
return 'Error: No story selected';
|
||||||
}
|
}
|
||||||
const source = args.source === 'lore'
|
|
||||||
? appState.currentStory.lore
|
|
||||||
: appState.currentStory.text;
|
|
||||||
|
|
||||||
const lines = source.split('\n');
|
const sources: { name: string; content: string }[] = [
|
||||||
const matches: { line: number; content: string }[] = [];
|
{ name: 'story', content: appState.currentStory.text },
|
||||||
|
{ name: 'lore', content: appState.currentStory.lore },
|
||||||
|
...appState.currentStory.characters.map(c => ({
|
||||||
|
name: `character:${c.name}`,
|
||||||
|
content: `${c.shortDescription}\n${c.description || ''}`.trim(),
|
||||||
|
})),
|
||||||
|
...appState.currentStory.locations.map(l => ({
|
||||||
|
name: `location:${l.name}`,
|
||||||
|
content: `${l.shortDescription}\n${l.description || ''}`.trim(),
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
const allMatches: { source: string; line: number; content: string }[] = [];
|
||||||
const pattern = new RegExp(args.pattern, args.case_sensitive ? 'g' : 'gi');
|
const pattern = new RegExp(args.pattern, args.case_sensitive ? 'g' : 'gi');
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
if (pattern.test(lines[i])) {
|
for (const src of sources) {
|
||||||
matches.push({ line: i + 1, content: lines[i].trim() });
|
const lines = src.content.split('\n');
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (pattern.test(lines[i])) {
|
||||||
|
allMatches.push({
|
||||||
|
source: src.name,
|
||||||
|
line: i + 1,
|
||||||
|
content: lines[i].trim(),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (matches.length === 0) {
|
|
||||||
|
if (allMatches.length === 0) {
|
||||||
return `No matches found for pattern: ${args.pattern}`;
|
return `No matches found for pattern: ${args.pattern}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const limit = args.limit ?? 20;
|
const limit = args.limit ?? 20;
|
||||||
const truncated = matches.length > limit;
|
const truncated = allMatches.length > limit;
|
||||||
const displayed = truncated ? matches.slice(0, limit) : matches;
|
const displayed = truncated ? allMatches.slice(0, limit) : allMatches;
|
||||||
let result = `Found ${matches.length} match(es) for pattern "${args.pattern}":\n`;
|
|
||||||
result += displayed.map(m => `Line ${m.line}: ${m.content}`).join('\n');
|
let result = `Found ${allMatches.length} match(es) for pattern "${args.pattern}":\n`;
|
||||||
|
result += displayed.map(m => `[${m.source}] Line ${m.line}: ${m.content}`).join('\n');
|
||||||
if (truncated) {
|
if (truncated) {
|
||||||
result += `\n... and ${matches.length - limit} more match(es)`;
|
result += `\n... and ${allMatches.length - limit} more match(es)`;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
description: 'Search for a pattern in the story text or lore',
|
description: 'Search for a pattern in the story text, lore, characters, and locations',
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
pattern: Type.String({ description: 'The regex pattern to search for' }),
|
pattern: Type.String({ description: 'The regex pattern to search for' }),
|
||||||
case_sensitive: Type.Optional(Type.Boolean({ description: 'If true, search is case-sensitive (default: false)' })),
|
case_sensitive: Type.Optional(Type.Boolean({ description: 'If true, search is case-sensitive (default: false)' })),
|
||||||
limit: Type.Optional(Type.Integer({ description: 'Maximum number of matches to return (default: 20)' })),
|
limit: Type.Optional(Type.Integer({ description: 'Maximum number of matches to return (default: 20)' })),
|
||||||
source: Type.Optional(Type.Enum(['lore', 'story'],
|
|
||||||
{ description: 'Source to search (story or lore, default: story)' },
|
|
||||||
)),
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue