From 1b9d03592125d3dff5d753f7286f9d652d5b87fb Mon Sep 17 00:00:00 2001 From: glu Date: Wed, 13 Nov 2024 16:36:12 -0500 Subject: [PATCH] fix: bugs (#16) --- .changeset/tough-dolls-teach.md | 5 +++ packages/ai/src/builder.ts | 58 ++++++++++++++------------- packages/ai/src/examples/analytics.ts | 4 +- packages/ai/src/linter.ts | 45 +++++++++++++-------- 4 files changed, 66 insertions(+), 46 deletions(-) create mode 100644 .changeset/tough-dolls-teach.md diff --git a/.changeset/tough-dolls-teach.md b/.changeset/tough-dolls-teach.md new file mode 100644 index 0000000..a68cb0b --- /dev/null +++ b/.changeset/tough-dolls-teach.md @@ -0,0 +1,5 @@ +--- +"@fatduckai/ai": patch +--- + +fixed empty content bug diff --git a/packages/ai/src/builder.ts b/packages/ai/src/builder.ts index adfe09d..0c028f2 100644 --- a/packages/ai/src/builder.ts +++ b/packages/ai/src/builder.ts @@ -16,7 +16,7 @@ export class PromptBuilder { constructor(template: string, options: PromptBuilderOptions = {}) { this.template = template; - this.linter = new Linter(this.context); + this.linter = new Linter(); this.options = { validateOnBuild: true, @@ -27,16 +27,23 @@ export class PromptBuilder { } withContext(context: Record): this { - Object.entries(context).forEach(([key, value]) => { - if (value === undefined || value === null) { - throw new Error( - `Context value for "${key}" cannot be undefined or null` - ); - } - }); + // Don't validate values if allowEmptyContent is true + if (!this.options.allowEmptyContent) { + Object.entries(context).forEach(([key, value]) => { + if ( + value === undefined || + value === null || + value.toString().trim() === "" + ) { + throw new Error( + `Empty content not allowed for "${key}". Set allowEmptyContent to true to allow empty, null, or undefined values.` + ); + } + }); + } this.context = { ...this.context, ...context }; - this.linter = new Linter(this.context); // Update linter with new context + this.linter = new Linter(this.context, this.options.allowEmptyContent); return this; } @@ -69,11 +76,13 @@ export class PromptBuilder { build(): ChatMessage[] { try { - // Still validate but don't throw if (this.options.validateOnBuild) { const validation = this.validate(); - - // Only throw if throwOnWarnings is true and we have warnings + if (!validation.isValid) { + throw new Error( + `Template validation failed:\n${validation.errors.join("\n")}` + ); + } if (this.options.throwOnWarnings && validation.warnings.length > 0) { throw new Error( `Template has warnings:\n${validation.warnings.join("\n")}` @@ -93,10 +102,12 @@ export class PromptBuilder { .map((block) => { let content = block.content; - // Replace variables that exist in context, leave others as is + // Replace variables, converting undefined/null to empty string if allowEmptyContent is true Object.entries(this.context).forEach(([key, value]) => { const regex = new RegExp(`<${key}>`, "g"); - content = content.replace(regex, String(value)); + const replacement = + value === undefined || value === null ? "" : String(value); + content = content.replace(regex, replacement); }); if (!content.trim() && !this.options.allowEmptyContent) { @@ -117,20 +128,11 @@ export class PromptBuilder { return message; }); } catch (error) { - // Instead of throwing, return the blocks with unreplaced variables - const parsed = Parser.parse(this.template); - return parsed.blocks - .filter( - (block): block is Block => - block.type === "system" || - block.type === "user" || - block.type === "assistant" - ) - .map((block) => ({ - role: block.type, - content: block.content.trim(), - ...(block.name ? { name: block.name } : {}), - })); + throw new Error( + `Failed to build prompt: ${ + error instanceof Error ? error.message : String(error) + }` + ); } } } diff --git a/packages/ai/src/examples/analytics.ts b/packages/ai/src/examples/analytics.ts index c09f63b..c17652b 100644 --- a/packages/ai/src/examples/analytics.ts +++ b/packages/ai/src/examples/analytics.ts @@ -18,8 +18,8 @@ What are the key trends?`; .join("\n"); console.log("before"); const builder = new PromptBuilder(template, { - allowEmptyContent: true, - }).withContext({ data: formattedData }); + allowEmptyContent: false, + }).withContext({ data: null }); const validation = await builder.validate(); console.log("validation", validation); const messages = await builder.build(); diff --git a/packages/ai/src/linter.ts b/packages/ai/src/linter.ts index a9a178e..69cf0bf 100644 --- a/packages/ai/src/linter.ts +++ b/packages/ai/src/linter.ts @@ -1,6 +1,9 @@ import { LintResult, ParsedTemplate } from "./types"; export class Linter { - constructor(private context: Record = {}) {} + constructor( + private context: Record = {}, + private allowEmptyContent: boolean = false + ) {} lint(template: ParsedTemplate): LintResult[] { const results: LintResult[] = []; @@ -18,21 +21,31 @@ export class Linter { }); } - // Check for undefined variables by comparing against context - const missingVariables = template.variables.filter( - (variable) => !(variable in this.context) - ); - - if (missingVariables.length > 0) { - results.push({ - message: `Required variables not provided: ${missingVariables.join( - ", " - )}`, - severity: "error", - line: 1, - column: 1, - }); - } + // Check variables + template.variables.forEach((variable) => { + if (!(variable in this.context)) { + results.push({ + message: `Required variable not provided: ${variable}`, + severity: "error", + line: this.findVariableLocation(variable, template.raw), + column: 1, + }); + } else if (!this.allowEmptyContent) { + const value = this.context[variable]; + if ( + value === undefined || + value === null || + value.toString().trim() === "" + ) { + results.push({ + message: `Empty content not allowed for variable: ${variable}`, + severity: "error", + line: this.findVariableLocation(variable, template.raw), + column: 1, + }); + } + } + }); return results; }