Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.
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
15 changes: 13 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down Expand Up @@ -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"
}
46 changes: 40 additions & 6 deletions packages/core/src/mustache.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -19,20 +19,54 @@ export async function interpolateVariables(
data: Record<string, any>,
options?: ImportTemplateOptions,
): Promise<string> {
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) {
// Extract default values from frontmatter parameters or inputs (prompty format)
const frontmatterDefaults: Record<string, any> = {};
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] = value;
}
}
mergedData = { ...frontmatterDefaults, ...mergedData };
}
}

// remove prompty roles
// https://github.com/microsoft/prompty/blob/main/runtime/prompty/prompty/parsers.py#L113C21-L113C77
content = content.replace(/^\s*(system|user|assistant)\s*:\s*$/gim, "\n");

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;
Expand Down
46 changes: 32 additions & 14 deletions packages/core/src/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -41,10 +48,10 @@ export function templateIdFromFileName(filename: string) {
*/
async function resolveMcpServersConfig(
mcpServers: McpServersConfig | undefined,
scriptPath: string
scriptPath: string,
): Promise<Record<string, Omit<McpServerConfig, "id" | "options">> | undefined> {
if (!mcpServers) return undefined;

if (typeof mcpServers === "string") {
// Handle file path - resolve relative to script directory
const configPath = resolve(dirname(scriptPath), mcpServers);
Expand All @@ -55,7 +62,9 @@ async function resolveMcpServersConfig(
if (config.mcpServers && typeof config.mcpServers === "object") {
return config.mcpServers as Record<string, Omit<McpServerConfig, "id" | "options">>;
} 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}`);
Expand All @@ -77,10 +86,10 @@ async function resolveMcpServersConfig(
*/
async function resolveMcpAgentServersConfig(
mcpAgentServers: McpAgentServersConfig | undefined,
scriptPath: string
scriptPath: string,
): Promise<Record<string, Omit<McpAgentServerConfig, "id" | "options">> | undefined> {
if (!mcpAgentServers) return undefined;

if (typeof mcpAgentServers === "string") {
// Handle file path - resolve relative to script directory
const configPath = resolve(dirname(scriptPath), mcpAgentServers);
Expand All @@ -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<string, Omit<McpAgentServerConfig, "id" | "options">>;
return config.mcpAgentServers as Record<
string,
Omit<McpAgentServerConfig, "id" | "options">
>;
} 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}`);
Expand Down Expand Up @@ -152,11 +166,15 @@ function parsePromptScriptTools(jsSource: string) {
*/
function extractFrontmatterParameters(content: string): Record<string, any> | 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;
}

/**
Expand Down Expand Up @@ -212,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;
}
98 changes: 96 additions & 2 deletions packages/core/test/mustache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,106 @@ 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.`);
});

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.`);
});

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`);
});
});