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

base validator #35

Merged
merged 10 commits into from
Feb 3, 2024
Merged
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
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,9 @@ dist
# build
packages/*/build/

packages/test
packages/test
packages/core/.DS_Store
packages/core/src/.DS_Store
packages/core/src/events/.DS_Store
packages/core/src/events/input/.DS_Store
packages/core/src/utils/.DS_Store
1 change: 1 addition & 0 deletions packages/core/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ utils/nanoid.d.ts
index.cjs
index.js
index.d.ts
yarn.lock
AlveinTeng marked this conversation as resolved.
Show resolved Hide resolved
123 changes: 123 additions & 0 deletions packages/core/src/events/input/promptTemplate/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Callable, CallableConfig } from "../../../record/callable";
import { variableValidator } from "../../../utils/promptTemplateValidator/variableValidator";
import { BaseEvent, BaseEventParams } from "../../base";
export interface PromptTemplateParams extends BaseEventParams {
/**
* Clearly declare the input variables inside the template
*/
inputVariables?: string[] | undefined;

/**
* the template string
*/
template?: string | "";

validator?: variableValidator | undefined;

partialVariables?: string[] | undefined;
}

export class BasePromptTemplate <
CallInput extends Record<string, any>,
CallOutput extends string,
CallOptions extends CallableConfig = CallableConfig,
>
extends BaseEvent<Record<string,any>, string, CallOptions>
implements PromptTemplateParams
{
_namespace: string[];
AlveinTeng marked this conversation as resolved.
Show resolved Hide resolved
inputVariables?: string[] | undefined;
template?: string | "";
validator?: variableValidator | undefined;
partialVariables?: string[] | undefined;


constructor(fields?: Partial<PromptTemplateParams>){
super(fields ?? {});
this.inputVariables = fields?.inputVariables ?? this.inputVariables;
this.template = fields?.template ?? this.template;
this.validator = fields?.validator ?? this.validator;
this.partialVariables = fields?.partialVariables ?? this.partialVariables;
if (this.template && this.inputVariables) {
this.isInputExists(this.template, this.inputVariables);
}
}
/**
* To validate whether the inputVariables is valid
* @param template
* @param inputVariables
*/
private isInputExists(template: string, inputVariables: string[]): void {
const variablePattern = /\{\{([^}]+)\}\}/g;
let match: RegExpExecArray | null;
const foundVariables: Set<string> = new Set();

while ((match = variablePattern.exec(template)) !== null) {
foundVariables.add(match[1]);
}

inputVariables.forEach(variable => {
if (!foundVariables.has(variable)) {
throw new Error(`Variable '${variable}' is declared but not used in the template.`);
}
});
}

private validateInput(input: CallInput): boolean{
if (this.validator) {
const validationResult = this.validator.validate(input);
if (!validationResult.isValid) {
// throw new Error(validationResult.errorMessage);
return false;
}
}
return true;
}

public getTemplate() : string{
if (!this.template){
return "";
}
return this.template;
}

public addPrefix(prefix: string): void {
if (this.template) {
this.template = `${prefix}${this.template}`;
} else {
this.template = prefix;
}
}

// Method to add a suffix to the template
public addSuffix(suffix: string): void {
if (this.template) {
this.template += suffix;
} else {
this.template = suffix;
}
}

async invoke(input: CallInput, options?: Partial<CallOptions>): Promise<CallOutput> {
// log('invoke called');
if (!this.validateInput(input)){
// log('not valid');
throw new Error("the validation for inputValue failed");
}

let formattedTemplate = this.template || "";
// Iterate through each input variable and replace placeholders with actual values
this.inputVariables?.forEach(async variable => {
if (variable in input) {
const regex = new RegExp(`\\{\\{${variable}\\}\\}`, 'g');
formattedTemplate = formattedTemplate.replace(regex, input[variable]);
} else {
// If a required variable is missing in the input, throw an error
throw new Error(`Missing value for variable '${variable}'`);
}
});
return formattedTemplate as CallOutput;
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { expect, jest, test } from '@jest/globals';
import { BasePromptTemplate } from '../base';
import { VariableValidator } from '../../../../utils/promptTemplateValidator/variableValidator';
test('declare not exist input variables', async () =>{
const inputVariables = ['usedVariable', 'unusedVariable'];
const template = 'This is a template with {{usedVariable}}';
expect(() =>{
new BasePromptTemplate({
template: template,
inputVariables: inputVariables
})
}).toThrow('Variable \'unusedVariable\' is declared but not used in the template.');
});
test('validator', async() => {
const isString = (s) => typeof s === 'string';
let validator;
validator = new VariableValidator(['name']);
validator.addSpecificRule('name', isString);
const promptTemplate = new BasePromptTemplate({
template: "{{name}}",
inputVariables: ["name"],
validator:validator
});
await expect(promptTemplate.invoke({'name': 1})).rejects.toThrow("the validation for inputValue failed");
});
test('addPrefix and addSuffix methods', async () => {
const promptTemplate = new BasePromptTemplate({
template: "{{name}}",
inputVariables: ["name"]
});
promptTemplate.addPrefix("Hello, ");
promptTemplate.addSuffix("!");
const formattedString = await promptTemplate.invoke({ name: "Alice" });
expect(formattedString).toBe("Hello, Alice!");
});
test('addPrefix and addSuffix methods', async () => {
const promptTemplate = new BasePromptTemplate({
template: "{{name}}",
inputVariables: ["name"]
});
promptTemplate.addPrefix("Hello, ");
promptTemplate.addSuffix("!");
const formattedString = await promptTemplate.invoke({ name: "Alice" });
expect(formattedString).toBe("Hello, Alice!");
});
Original file line number Diff line number Diff line change
Expand Up @@ -483,4 +483,4 @@ describe('TokenTextSplitter', () => {

expect(newContexts.map((c) => c.pageContent)).toStrictEqual([]);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Callable, CallableConfig } from "../../record/callable";

type ValidationFunction = (input: any) => boolean;
type ValidationResult = { isValid: boolean; errorMessage?: string };

interface ValidatorRules {
default?: ValidationFunction;
specific?: { [key: string]: ValidationFunction };
exclude?: string[];
}

export class VariableValidator {
private rules: ValidatorRules;
private validatableVariables: Set<string>;
constructor(validatableVariables: string[], rules: ValidatorRules = {}) {
this.validatableVariables = new Set(validatableVariables);
this.rules = rules;
}

//get the attributes of baseValidator class
getAttributes(): object {
return {
validatableVariables: Array.from(this.validatableVariables),
rules: this.rules,
};
}

private hasDefaultRule(key: string): boolean | undefined {
return this.rules.default && !this.rules.exclude?.includes(key);
}

// Function to add a variable to the exclude list
addExcludeVariable(variable: string): void {
if (!this.rules.exclude) {
this.rules.exclude = [];
}
if (!this.rules.exclude.includes(variable)) {
this.rules.exclude.push(variable);
}
}

// Function to remove a variable from the exclude list
removeExcludeVariable(variable: string): void {
if (this.rules.exclude) {
this.rules.exclude = this.rules.exclude.filter(v => v !== variable);
}
}

// Adds a new specific validation rule
addSpecificRule(key: string, validationFunction: ValidationFunction): void {
if (!this.validatableVariables.has(key)) {
throw new Error(`Rule addition failed: '${key}' is not a validatable variable.`);
}
if (this.rules.specific?.[key]) {
throw new Error(`Conflict detected: A rule for '${key}' conflicts with another specific rule.`);
}
if (this.hasDefaultRule(key)) {
throw new Error(`Conflict detected: A rule for '${key}' conflicts with a default rule.`);
}
if (!this.rules.specific) {
this.rules.specific = {};
}
this.rules.specific[key] = validationFunction;
}

// Function to remove a specific rule for a given variable
removeSpecificRule(variable: string): void {
if (!this.rules.specific || !this.rules.specific[variable]) {
throw new Error(`No specific rule exists for variable '${variable}'`);
}

delete this.rules.specific[variable];
}

// Validates a given input object based on the added rules
validate(input: Record<string, any>): ValidationResult {
for (const key in input) {
if (this.rules.exclude?.includes(key)) {
continue;
}

const specificRule = this.rules.specific?.[key];
const ruleToApply = specificRule || this.rules.default;

if (ruleToApply && !ruleToApply(input[key])) {
return { isValid: false, errorMessage: `Validation failed for ${key}` };
}
}
return { isValid: true };
}
}

47 changes: 47 additions & 0 deletions packages/core/src/utils/tests/variableValidator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { expect, jest, test } from '@jest/globals';
import { VariableValidator } from '../promptTemplateValidator/variableValidator';

const isPositive = (n) => n > 0;
const isString = (s) => typeof s === 'string';

let validator;
validator = new VariableValidator(['age', 'name']);

test('add and remove specific rules', () => {
expect(() => validator.addSpecificRule('age', isPositive)).not.toThrow();

expect(() => validator.removeSpecificRule('age')).not.toThrow();
expect(() => validator.removeSpecificRule('age')).toThrow();
});

test('add and remove exclude variables', () => {
validator.addExcludeVariable('name');
expect(validator.getAttributes().rules.exclude).toContain('name');
validator.removeExcludeVariable('name');
expect(validator.getAttributes().rules.exclude).not.toContain('name');
});

test('validate with specific rule', () => {
validator.addSpecificRule('age', isPositive);
expect(validator.validate({ age: 5 })).toEqual({ isValid: true });
expect(validator.validate({ age: -3 })).toEqual({ isValid: false, errorMessage: 'Validation failed for age' });
});

test('validate with default rule', () => {
validator = new VariableValidator(['age', 'name'], { default: isString });
expect(validator.validate({ age: '25', name: 'John' })).toEqual({ isValid: true });
expect(validator.validate({ age: 25, name: 'John' })).toEqual({ isValid: false, errorMessage: 'Validation failed for age' });
});

test('add specific rule with default rule conflict and exclusion', () => {
const validator = new VariableValidator(['age', 'name'], { default: isString });

// Attempt to add a specific rule that conflicts with the default rule
expect(() => validator.addSpecificRule('age', isPositive)).toThrow();

// Exclude 'age' from default rule
validator.addExcludeVariable('age');

// Successfully add specific rule after exclusion
expect(() => validator.addSpecificRule('age', isPositive)).not.toThrow();
});
Loading
Loading