Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

All three brackets for variables #14465

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 45 additions & 6 deletions packages/ai-core/data/prompttemplate.tmLanguage.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,56 @@
"scopeName": "source.prompttemplate",
"patterns": [
{
"name": "variable.other.prompttemplate",
"begin": "{{",
"name": "invalid.illegal.mismatched.prompttemplate",
"match": "\\{\\{\\{[^}]*\\}\\}(?!\\})",
"captures": {
"0": {
"name": "invalid.illegal.bracket.mismatch"
}
}
},
{
"name": "invalid.illegal.mismatched.prompttemplate",
"match": "\\{\\{[^}]*\\}\\}\\}(?!\\})",
"captures": {
"0": {
"name": "invalid.illegal.bracket.mismatch"
}
}
},
{
"name": "variable.other.prompttemplate.double",
"begin": "\\{\\{",
"beginCaptures": {
"0": {
"name": "punctuation.definition.brace.begin"
"name": "punctuation.definition.variable.begin"
}
},
"end": "}}",
"end": "\\}\\}(?!\\})",
"endCaptures": {
"0": {
"name": "punctuation.definition.brace.end"
"name": "punctuation.definition.variable.end"
}
},
"patterns": [
{
"name": "keyword.control",
"match": "[a-zA-Z_][a-zA-Z0-9_]*"
}
]
},
{
"name": "variable.other.prompttemplate.triple",
"begin": "\\{\\{\\{",
"beginCaptures": {
"0": {
"name": "punctuation.definition.variable.begin"
}
},
"end": "\\}\\}\\}(?!\\})",
"endCaptures": {
"0": {
"name": "punctuation.definition.variable.end"
}
},
"patterns": [
Expand Down Expand Up @@ -49,4 +88,4 @@
"fileTypes": [
".prompttemplate"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import {
AIVariableService,
LanguageModel,
LanguageModelRegistry,
matchVariablesRegEx,
PROMPT_FUNCTION_REGEX,
PROMPT_VARIABLE_REGEX,
PromptCustomizationService,
PromptService,
} from '../../common';
Expand Down Expand Up @@ -182,7 +182,7 @@ export class AIAgentConfigurationWidget extends ReactWidget {
promptTemplates.forEach(template => {
const storedPrompt = this.promptService.getRawPrompt(template.id);
const prompt = storedPrompt?.template ?? template.template;
const variableMatches = [...prompt.matchAll(PROMPT_VARIABLE_REGEX)];
const variableMatches = matchVariablesRegEx(prompt);

variableMatches.forEach(match => {
const variableId = match[1];
Expand Down
10 changes: 8 additions & 2 deletions packages/ai-core/src/common/prompt-service-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

/** Should match the one from VariableResolverService. The format is `{{variableName:arg}}`. */
export const PROMPT_VARIABLE_REGEX = /\{\{\s*(.*?)\s*\}\}/g;
/** Should match the one from VariableResolverService. The format is `{{variableName:arg}}`. We allow {{}} and {{{}}} but no mixtures */
export const PROMPT_VARIABLE_TWO_BRACES_REGEX = /(?<!\{)\{\{\s*([^{}]+?)\s*\}\}(?!\})/g;
export const PROMPT_VARIABLE_THREE_BRACES_REGEX = /(?<!\{)\{\{\{\s*([^{}]+?)\s*\}\}\}(?!\})/g;
export function matchVariablesRegEx(template: string): RegExpMatchArray[] {
const twoBraceMatches = [...template.matchAll(PROMPT_VARIABLE_TWO_BRACES_REGEX)];
const threeBraceMatches = [...template.matchAll(PROMPT_VARIABLE_THREE_BRACES_REGEX)];
return twoBraceMatches.concat(threeBraceMatches);
}

/** Match function/tool references in the prompt. The format is `~{functionId}`. */
export const PROMPT_FUNCTION_REGEX = /\~\{\s*(.*?)\s*\}/g;
64 changes: 64 additions & 0 deletions packages/ai-core/src/common/prompt-service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@ describe('PromptService', () => {
promptService.storePrompt('1', 'Hello, {{name}}!');
promptService.storePrompt('2', 'Goodbye, {{name}}!');
promptService.storePrompt('3', 'Ciao, {{invalid}}!');
promptService.storePrompt('8', 'Hello, {{{name}}}');
});

it('should initialize prompts from PromptCollectionService', () => {
const allPrompts = promptService.getAllPrompts();
expect(allPrompts['1'].template).to.equal('Hello, {{name}}!');
expect(allPrompts['2'].template).to.equal('Goodbye, {{name}}!');
expect(allPrompts['3'].template).to.equal('Ciao, {{invalid}}!');
expect(allPrompts['8'].template).to.equal('Hello, {{{name}}}');
});

it('should retrieve raw prompt by id', () => {
Expand Down Expand Up @@ -95,4 +97,66 @@ describe('PromptService', () => {
expect(prompt?.text).to.equal('Hello, John!');
}
});

it('should retrieve raw prompt by id (three bracket)', () => {
const rawPrompt = promptService.getRawPrompt('8');
expect(rawPrompt?.template).to.equal('Hello, {{{name}}}');
});

it('should correctly replace variables (three brackets)', async () => {
const formattedPrompt = await promptService.getPrompt('8');
expect(formattedPrompt?.text).to.equal('Hello, Jane');
});

it('should ignore whitespace in variables (three bracket)', async () => {
promptService.storePrompt('9', 'Hello, {{{name }}}');
promptService.storePrompt('10', 'Hello, {{{ name}}}');
promptService.storePrompt('11', 'Hello, {{{ name }}}');
promptService.storePrompt('12', 'Hello, {{{ name }}}');
for (let i = 9; i <= 12; i++) {
const prompt = await promptService.getPrompt(`${i}`, { name: 'John' });
expect(prompt?.text).to.equal('Hello, John');
}
});

it('should ignore invalid prompts with unmatched brackets', async () => {
promptService.storePrompt('9', 'Hello, {{name');
promptService.storePrompt('10', 'Hello, {{{name');
promptService.storePrompt('11', 'Hello, name}}}}');
const prompt1 = await promptService.getPrompt('9', { name: 'John' });
expect(prompt1?.text).to.equal('Hello, {{name'); // Not matching due to missing closing brackets

const prompt2 = await promptService.getPrompt('10', { name: 'John' });
expect(prompt2?.text).to.equal('Hello, {{{name'); // Matches pattern due to valid three-start-two-end brackets

const prompt3 = await promptService.getPrompt('11', { name: 'John' });
expect(prompt3?.text).to.equal('Hello, name}}}}'); // Extra closing bracket, does not match cleanly
});

it('should handle a mixture of two and three brackets correctly', async () => {
promptService.storePrompt('12', 'Hi, {{name}}}'); // (invalid)
promptService.storePrompt('13', 'Hello, {{{name}}'); // (invalid)
promptService.storePrompt('14', 'Greetings, {{{name}}}}'); // (invalid)
promptService.storePrompt('15', 'Bye, {{{{name}}}'); // (invalid)
promptService.storePrompt('16', 'Ciao, {{{{name}}}}'); // (invalid)
promptService.storePrompt('17', 'Hi, {{name}}! {{{name}}}'); // Mixed valid patterns

const prompt12 = await promptService.getPrompt('12', { name: 'John' });
expect(prompt12?.text).to.equal('Hi, {{name}}}');

const prompt13 = await promptService.getPrompt('13', { name: 'John' });
expect(prompt13?.text).to.equal('Hello, {{{name}}');

const prompt14 = await promptService.getPrompt('14', { name: 'John' });
expect(prompt14?.text).to.equal('Greetings, {{{name}}}}');

const prompt15 = await promptService.getPrompt('15', { name: 'John' });
expect(prompt15?.text).to.equal('Bye, {{{{name}}}');

const prompt16 = await promptService.getPrompt('16', { name: 'John' });
expect(prompt16?.text).to.equal('Ciao, {{{{name}}}}');

const prompt17 = await promptService.getPrompt('17', { name: 'John' });
expect(prompt17?.text).to.equal('Hi, John! John');
});
});
9 changes: 7 additions & 2 deletions packages/ai-core/src/common/prompt-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { AIVariableService } from './variable-service';
import { ToolInvocationRegistry } from './tool-invocation-registry';
import { toolRequestToPromptText } from './language-model-util';
import { ToolRequest } from './language-model';
import { PROMPT_VARIABLE_REGEX, PROMPT_FUNCTION_REGEX } from './prompt-service-util';
import { PROMPT_FUNCTION_REGEX, matchVariablesRegEx } from './prompt-service-util';

export interface PromptTemplate {
id: string;
Expand Down Expand Up @@ -181,13 +181,18 @@ export class PromptServiceImpl implements PromptService {
getDefaultRawPrompt(id: string): PromptTemplate | undefined {
return this._prompts[id];
}

matchVariables(template: string): RegExpMatchArray[] {
return matchVariablesRegEx(template);
}

async getPrompt(id: string, args?: { [key: string]: unknown }): Promise<ResolvedPromptTemplate | undefined> {
const prompt = this.getRawPrompt(id);
if (prompt === undefined) {
return undefined;
}

const matches = [...prompt.template.matchAll(PROMPT_VARIABLE_REGEX)];
const matches = this.matchVariables(prompt.template);
const variableAndArgReplacements = await Promise.all(matches.map(async match => {
const completeText = match[0];
const variableAndArg = match[1];
Expand Down
Loading