From 7a3d3afb53ae412177f0f8589d08a48dbfcd1dde Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:00:51 +0000 Subject: [PATCH 1/5] Initial plan From b4895b9ca8165da4a0fcefe1040d318ff2adf9f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:07:04 +0000 Subject: [PATCH 2/5] Initial analysis and planning for frontmatter parameter interpolation fix Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- packages/core/package.json | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index a80cf38b72..719b5508bc 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -4,7 +4,10 @@ "version": "2.4.0", "license": "MIT", "type": "module", - "repository": "github:microsoft/genaiscript", + "repository": { + "type": "git", + "url": "git+https://github.com/microsoft/genaiscript.git" + }, "bugs": { "url": "https://github.com/microsoft/genaiscript/issues" }, @@ -175,5 +178,13 @@ }, "main": "./dist/commonjs/index.js", "types": "./dist/commonjs/index.d.ts", - "module": "./dist/esm/index.js" + "module": "./dist/esm/index.js", + "description": "Core package for the GenAIScript project, which provides the foundational libraries and tools to build and execute GenAIScript code.", + "directories": { + "doc": "docs", + "test": "test" + }, + "keywords": [], + "author": "", + "homepage": "https://github.com/microsoft/genaiscript#readme" } From 2ca3920b327cb524a8695ce67af13d978f5f4a70 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:10:11 +0000 Subject: [PATCH 3/5] Fix frontmatter parameter interpolation in mustache.ts Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- packages/core/src/mustache.ts | 32 +++++++++++++---- packages/core/test/mustache.test.ts | 56 +++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/packages/core/src/mustache.ts b/packages/core/src/mustache.ts index c6bd476a16..f0f6d817cb 100644 --- a/packages/core/src/mustache.ts +++ b/packages/core/src/mustache.ts @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { splitMarkdown } from "./frontmatter.js"; +import { splitMarkdown, frontmatterTryParse } from "./frontmatter.js"; import Mustache from "mustache"; import { jinjaRender } from "./jinja.js"; import type { ImportTemplateOptions } from "./types.js"; /** * Processes a markdown string by applying Mustache or Jinja templating. - * Removes frontmatter, prompty roles, and XML tags before interpolation. + * Extracts frontmatter parameters and merges them with provided data before interpolation. * @param md The markdown string to process. * @param data The data for variable interpolation. * @param options Configuration for templating format, e.g., Mustache or Jinja. @@ -19,10 +19,30 @@ export async function interpolateVariables( data: Record, options?: ImportTemplateOptions, ): Promise { - if (!md || !data) return md; + if (!md) return md; const { format } = options || {}; - // remove frontmatter + + // Extract frontmatter and content let { content } = splitMarkdown(md); + + // Extract parameters from frontmatter and merge with provided data + const frontmatter = frontmatterTryParse(md); + let mergedData = { ...(data ?? {}) }; + + if (frontmatter?.value?.parameters) { + // Extract default values from frontmatter parameters + const frontmatterDefaults: Record = {}; + for (const [key, param] of Object.entries(frontmatter.value.parameters)) { + if (typeof param === 'object' && param !== null && 'default' in param) { + // Only use frontmatter default if no data provided for this key + if (!(key in mergedData)) { + frontmatterDefaults[key] = param.default; + } + } + } + // Merge frontmatter defaults with provided data (data takes precedence) + mergedData = { ...frontmatterDefaults, ...mergedData }; + } // remove prompty roles // https://github.com/microsoft/prompty/blob/main/runtime/prompty/prompty/parsers.py#L113C21-L113C77 @@ -31,8 +51,8 @@ export async function interpolateVariables( if (content) { // remove xml tags // https://humanloop.com/docs/prompt-file-format - if (format === "jinja") content = jinjaRender(content, data ?? {}); - else content = Mustache.render(content, data ?? {}); + if (format === "jinja") content = jinjaRender(content, mergedData); + else content = Mustache.render(content, mergedData); } return content; diff --git a/packages/core/test/mustache.test.ts b/packages/core/test/mustache.test.ts index 6a788dbd82..c409584324 100644 --- a/packages/core/test/mustache.test.ts +++ b/packages/core/test/mustache.test.ts @@ -128,4 +128,60 @@ Hello {{name}}! This message will repeat {{count}} times.`; assert.strictEqual(output, `Hello GenAI! This message will repeat 5 times.`); }); + + test("should use frontmatter parameter defaults when no data provided", async () => { + const md = `--- +parameters: + name: + type: string + default: "World" + count: + type: number + default: 3 +--- +Hello {{name}}! This message will repeat {{count}} times.`; + + const output = await interpolateVariables(md, {}); + + assert.strictEqual(output, `Hello World! This message will repeat 3 times.`); + }); + + test("should handle partial data with frontmatter defaults", async () => { + const md = `--- +parameters: + name: + type: string + default: "World" + count: + type: number + default: 3 +--- +Hello {{name}}! This message will repeat {{count}} times.`; + + const output = await interpolateVariables(md, { + name: "GenAI" + // count should use default from frontmatter + }); + + assert.strictEqual(output, `Hello GenAI! This message will repeat 3 times.`); + }); + + test("should handle frontmatter parameters with Jinja format", async () => { + const md = `--- +parameters: + name: + type: string + default: "World" + count: + type: number + default: 3 +--- +Hello {{ name }}! This message will repeat {{ count }} times.`; + + const output = await interpolateVariables(md, { + name: "GenAI" + }, { format: "jinja" }); + + assert.strictEqual(output, `Hello GenAI! This message will repeat 3 times.`); + }); }); From 81fcf53b92e9e123530db6390e04b77d889ea7ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:13:55 +0000 Subject: [PATCH 4/5] Enhance frontmatter parameter support for prompty format Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- packages/core/src/mustache.ts | 30 ++++++++++++++++++------- packages/core/src/template.ts | 8 +++++-- packages/core/test/mustache.test.ts | 34 +++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/packages/core/src/mustache.ts b/packages/core/src/mustache.ts index f0f6d817cb..19c237409c 100644 --- a/packages/core/src/mustache.ts +++ b/packages/core/src/mustache.ts @@ -29,19 +29,33 @@ export async function interpolateVariables( const frontmatter = frontmatterTryParse(md); let mergedData = { ...(data ?? {}) }; - if (frontmatter?.value?.parameters) { - // Extract default values from frontmatter parameters + if (frontmatter?.value) { + // Extract default values from frontmatter parameters or inputs (prompty format) const frontmatterDefaults: Record = {}; - for (const [key, param] of Object.entries(frontmatter.value.parameters)) { - if (typeof param === 'object' && param !== null && 'default' in param) { - // Only use frontmatter default if no data provided for this key + const parameterSource = frontmatter.value.parameters || frontmatter.value.inputs; + + if (parameterSource) { + for (const [key, param] of Object.entries(parameterSource)) { + if (typeof param === 'object' && param !== null && 'default' in param) { + // Only use frontmatter default if no data provided for this key + if (!(key in mergedData)) { + frontmatterDefaults[key] = param.default; + } + } + } + // Merge frontmatter defaults with provided data (data takes precedence) + mergedData = { ...frontmatterDefaults, ...mergedData }; + } + + // Handle prompty sample data as defaults + if (frontmatter.value.sample && typeof frontmatter.value.sample === 'object') { + for (const [key, value] of Object.entries(frontmatter.value.sample)) { if (!(key in mergedData)) { - frontmatterDefaults[key] = param.default; + frontmatterDefaults[key] = value; } } + mergedData = { ...frontmatterDefaults, ...mergedData }; } - // Merge frontmatter defaults with provided data (data takes precedence) - mergedData = { ...frontmatterDefaults, ...mergedData }; } // remove prompty roles diff --git a/packages/core/src/template.ts b/packages/core/src/template.ts index eefabb8526..0c187edd6f 100644 --- a/packages/core/src/template.ts +++ b/packages/core/src/template.ts @@ -152,11 +152,15 @@ function parsePromptScriptTools(jsSource: string) { */ function extractFrontmatterParameters(content: string): Record | undefined { const fm = frontmatterTryParse(content); - if (!fm?.value?.parameters) return undefined; + if (!fm?.value) return undefined; + + // Handle both 'parameters' and 'inputs' (prompty format) + const parameterSource = fm.value.parameters || fm.value.inputs; + if (!parameterSource) return undefined; // Return the parameters directly - they should already be in the correct format // with type definitions like { type: "string", default: "value" } - return fm.value.parameters; + return parameterSource; } /** diff --git a/packages/core/test/mustache.test.ts b/packages/core/test/mustache.test.ts index c409584324..5a36293240 100644 --- a/packages/core/test/mustache.test.ts +++ b/packages/core/test/mustache.test.ts @@ -184,4 +184,38 @@ Hello {{ name }}! This message will repeat {{ count }} times.`; assert.strictEqual(output, `Hello GenAI! This message will repeat 3 times.`); }); + + test("should handle prompty format with inputs and sample", async () => { + const md = `--- +name: Basic Prompt +description: A basic prompt +inputs: + question: + type: string +sample: + "question": "What is the capital of France?" +--- +Question: {{question}}`; + + const output = await interpolateVariables(md, {}); + + assert.strictEqual(output, `Question: What is the capital of France?`); + }); + + test("should handle prompty format with data override", async () => { + const md = `--- +inputs: + question: + type: string +sample: + "question": "Default question" +--- +Question: {{question}}`; + + const output = await interpolateVariables(md, { + question: "Custom question" + }); + + assert.strictEqual(output, `Question: Custom question`); + }); }); From ef12503f97cc4537865b152c12dac10a0779c4cb Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 Aug 2025 16:20:18 +0000 Subject: [PATCH 5/5] genai: /docs [skip ci] --- packages/core/src/mustache.ts | 14 +++++----- packages/core/src/template.ts | 40 +++++++++++++++++++---------- packages/core/test/mustache.test.ts | 38 +++++++++++++++------------ 3 files changed, 55 insertions(+), 37 deletions(-) diff --git a/packages/core/src/mustache.ts b/packages/core/src/mustache.ts index 19c237409c..406552b2c1 100644 --- a/packages/core/src/mustache.ts +++ b/packages/core/src/mustache.ts @@ -21,22 +21,22 @@ export async function interpolateVariables( ): Promise { if (!md) return md; const { format } = options || {}; - + // Extract frontmatter and content let { content } = splitMarkdown(md); - + // Extract parameters from frontmatter and merge with provided data const frontmatter = frontmatterTryParse(md); let mergedData = { ...(data ?? {}) }; - + if (frontmatter?.value) { // Extract default values from frontmatter parameters or inputs (prompty format) const frontmatterDefaults: Record = {}; const parameterSource = frontmatter.value.parameters || frontmatter.value.inputs; - + if (parameterSource) { for (const [key, param] of Object.entries(parameterSource)) { - if (typeof param === 'object' && param !== null && 'default' in param) { + if (typeof param === "object" && param !== null && "default" in param) { // Only use frontmatter default if no data provided for this key if (!(key in mergedData)) { frontmatterDefaults[key] = param.default; @@ -46,9 +46,9 @@ export async function interpolateVariables( // Merge frontmatter defaults with provided data (data takes precedence) mergedData = { ...frontmatterDefaults, ...mergedData }; } - + // Handle prompty sample data as defaults - if (frontmatter.value.sample && typeof frontmatter.value.sample === 'object') { + if (frontmatter.value.sample && typeof frontmatter.value.sample === "object") { for (const [key, value] of Object.entries(frontmatter.value.sample)) { if (!(key in mergedData)) { frontmatterDefaults[key] = value; diff --git a/packages/core/src/template.ts b/packages/core/src/template.ts index 0c187edd6f..a3b174ddf5 100644 --- a/packages/core/src/template.ts +++ b/packages/core/src/template.ts @@ -15,7 +15,14 @@ import { deleteUndefinedValues } from "./cleaners.js"; import { markdownScriptParse } from "./markdownscript.js"; import { readJSON } from "./fs.js"; import { frontmatterTryParse } from "./frontmatter.js"; -import type { PromptArgs, PromptScript, McpServersConfig, McpServerConfig, McpAgentServersConfig, McpAgentServerConfig } from "./types.js"; +import type { + PromptArgs, + PromptScript, + McpServersConfig, + McpServerConfig, + McpAgentServersConfig, + McpAgentServerConfig, +} from "./types.js"; import { basename, resolve, dirname } from "node:path"; import { readText } from "./fs.js"; @@ -41,10 +48,10 @@ export function templateIdFromFileName(filename: string) { */ async function resolveMcpServersConfig( mcpServers: McpServersConfig | undefined, - scriptPath: string + scriptPath: string, ): Promise> | undefined> { if (!mcpServers) return undefined; - + if (typeof mcpServers === "string") { // Handle file path - resolve relative to script directory const configPath = resolve(dirname(scriptPath), mcpServers); @@ -55,7 +62,9 @@ async function resolveMcpServersConfig( if (config.mcpServers && typeof config.mcpServers === "object") { return config.mcpServers as Record>; } else { - throw new Error(`Invalid MCP server configuration format in ${configPath}. Configuration must have a root 'mcpServers' field.`); + throw new Error( + `Invalid MCP server configuration format in ${configPath}. Configuration must have a root 'mcpServers' field.`, + ); } } else { throw new Error(`Invalid MCP server configuration format in ${configPath}`); @@ -77,10 +86,10 @@ async function resolveMcpServersConfig( */ async function resolveMcpAgentServersConfig( mcpAgentServers: McpAgentServersConfig | undefined, - scriptPath: string + scriptPath: string, ): Promise> | undefined> { if (!mcpAgentServers) return undefined; - + if (typeof mcpAgentServers === "string") { // Handle file path - resolve relative to script directory const configPath = resolve(dirname(scriptPath), mcpAgentServers); @@ -89,9 +98,14 @@ async function resolveMcpAgentServersConfig( if (typeof config === "object" && config !== null) { // Require Claude format with root mcpAgentServers field if (config.mcpAgentServers && typeof config.mcpAgentServers === "object") { - return config.mcpAgentServers as Record>; + return config.mcpAgentServers as Record< + string, + Omit + >; } else { - throw new Error(`Invalid MCP agent server configuration format in ${configPath}. Configuration must have a root 'mcpAgentServers' field.`); + throw new Error( + `Invalid MCP agent server configuration format in ${configPath}. Configuration must have a root 'mcpAgentServers' field.`, + ); } } else { throw new Error(`Invalid MCP agent server configuration format in ${configPath}`); @@ -153,11 +167,11 @@ function parsePromptScriptTools(jsSource: string) { function extractFrontmatterParameters(content: string): Record | undefined { const fm = frontmatterTryParse(content); if (!fm?.value) return undefined; - + // Handle both 'parameters' and 'inputs' (prompty format) const parameterSource = fm.value.parameters || fm.value.inputs; if (!parameterSource) return undefined; - + // Return the parameters directly - they should already be in the correct format // with type definitions like { type: "string", default: "value" } return parameterSource; @@ -216,16 +230,16 @@ async function parsePromptTemplateCore(filename: string, content: string) { */ export async function parsePromptScript(filename: string, content: string) { const script = await parsePromptTemplateCore(filename, content); - + // Extract frontmatter parameters from markdown files and merge them // This handles the case where markdown scripts define parameters in frontmatter const frontmatterParameters = extractFrontmatterParameters(content); if (frontmatterParameters) { script.parameters = { ...(script.parameters || {}), - ...frontmatterParameters + ...frontmatterParameters, }; } - + return script; } diff --git a/packages/core/test/mustache.test.ts b/packages/core/test/mustache.test.ts index 5a36293240..89bd04589d 100644 --- a/packages/core/test/mustache.test.ts +++ b/packages/core/test/mustache.test.ts @@ -120,12 +120,12 @@ parameters: default: 3 --- Hello {{name}}! This message will repeat {{count}} times.`; - + const output = await interpolateVariables(md, { name: "GenAI", count: 5, }); - + assert.strictEqual(output, `Hello GenAI! This message will repeat 5 times.`); }); @@ -140,9 +140,9 @@ parameters: default: 3 --- Hello {{name}}! This message will repeat {{count}} times.`; - + const output = await interpolateVariables(md, {}); - + assert.strictEqual(output, `Hello World! This message will repeat 3 times.`); }); @@ -157,12 +157,12 @@ parameters: default: 3 --- Hello {{name}}! This message will repeat {{count}} times.`; - + const output = await interpolateVariables(md, { - name: "GenAI" + name: "GenAI", // count should use default from frontmatter }); - + assert.strictEqual(output, `Hello GenAI! This message will repeat 3 times.`); }); @@ -177,11 +177,15 @@ parameters: default: 3 --- Hello {{ name }}! This message will repeat {{ count }} times.`; - - const output = await interpolateVariables(md, { - name: "GenAI" - }, { format: "jinja" }); - + + const output = await interpolateVariables( + md, + { + name: "GenAI", + }, + { format: "jinja" }, + ); + assert.strictEqual(output, `Hello GenAI! This message will repeat 3 times.`); }); @@ -196,9 +200,9 @@ sample: "question": "What is the capital of France?" --- Question: {{question}}`; - + const output = await interpolateVariables(md, {}); - + assert.strictEqual(output, `Question: What is the capital of France?`); }); @@ -211,11 +215,11 @@ sample: "question": "Default question" --- Question: {{question}}`; - + const output = await interpolateVariables(md, { - question: "Custom question" + question: "Custom question", }); - + assert.strictEqual(output, `Question: Custom question`); }); });