Skip to content

Create-Inc/teleprompt

Repository files navigation

teleprompt

Composable, section-based LLM system prompts.

Each section owns its content and conditional logic. Sections can be reused across prompt variants, conditionally included, and rendered as plain text or XML.

pnpm add @anythingai/teleprompt

Quick Start

import { PromptBuilder, section, type PromptContext } from '@anythingai/teleprompt';

// Define your context shape
type MyFlags = { webSearchEnabled: boolean };
type MyVars = { assistantName: string };
type MyContext = PromptContext<MyFlags, MyVars>;

// Static section — no context needed, no type parameter
const guidelines = section('guidelines', () => `# Guidelines
- Be concise and direct.
- Cite sources when making factual claims.
- Ask for clarification when a request is ambiguous.`);

// Dynamic section — uses context
const identity = section('identity', (ctx: MyContext) =>
  `You are ${ctx.vars.assistantName}, a helpful AI assistant.`
);

// Conditional section — return null to exclude
const webSearch = section('web-search', (ctx: MyContext) => {
  if (!ctx.flags.webSearchEnabled) return null;
  return `You have access to web search. Use it when the user asks about
current events or information that may have changed after your training cutoff.`;
});

// Compose and build
const prompt = new PromptBuilder<MyContext>()
  .use(identity)
  .use(guidelines)
  .use(webSearch)
  .build({
    flags: { webSearchEnabled: true },
    vars: { assistantName: 'Daniel' },
  });

Sections

section(id, render) takes an id and a render function. Return a string to include, null to exclude:

// Always included
const rules = section('rules', () => 'Be helpful and concise.');

// Conditional — null means excluded
const citation = section('citation', (ctx: MyContext) => {
  if (!ctx.flags.citationEnabled) return null;
  return 'Always include citations with links when referencing external sources.';
});

Sections render in the order you call .use(). To reorder, change the call order.

Static sections (no type parameter) work in any builder:

const disclaimer = section('disclaimer', () => 'Responses are not legal advice.');

// Works in any builder regardless of context type
new PromptBuilder<MyContext>().use(disclaimer)
new PromptBuilder<OtherContext>().use(disclaimer)

Context

Sections receive a typed context with boolean flags and arbitrary variables:

type MyFlags = {
  webSearchEnabled: boolean;
  citationEnabled: boolean;
};

type MyVars = {
  assistantName: string;
  language: string;
};

type MyContext = PromptContext<MyFlags, MyVars>;

PromptContext and PromptBuilder have defaults, so the type parameter is optional:

const builder = new PromptBuilder();

Build the context once and pass it to .build(ctx). Every section receives the same object.

Forking

Create variants from a shared base:

const base = new PromptBuilder<MyContext>()
  .use(identity)
  .use(guidelines)
  .use(tone);

// Customer support agent — adds escalation rules
const supportAgent = base.fork()
  .use(escalationPolicy)
  .use(ticketFormat);

// Code assistant — swaps guidelines, drops tone
const codeAssistant = base.fork()
  .without(guidelines)
  .without(tone)
  .use(codingGuidelines)
  .use(outputFormat);

Each fork is independent. Modifying one doesn't affect the others.

XML Format

Both Claude and Gemini recommend structuring prompts with XML tags. Pass { format: 'xml' } to .build() to wrap each section in <id> tags:

builder.build(ctx, { format: 'xml' })

The section id becomes the tag name. Content is left as-is inside the tags.

Groups

Group related sections together. In text mode, groups are transparent. In XML mode, they wrap children in tags:

const prompt = new PromptBuilder<MyContext>()
  .use(identity)
  .group('tools', b => b
    .use(webSearch)
    .use(calculator)
  )
  .use(guidelines)
  .build(ctx, { format: 'xml' });
<identity>
You are Daniel, a helpful AI assistant.
</identity>

<tools>
<web-search>
You have access to web search...
</web-search>

<calculator>
You can evaluate math expressions...
</calculator>
</tools>

<guidelines>
# Guidelines
...
</guidelines>

Groups can be nested:

builder.group('capabilities', b => b
  .group('tools', b => b
    .use(webSearch)
    .use(calculator)
  )
  .group('integrations', b => b
    .use(slack)
    .use(linear)
  )
)

Mutually Exclusive Sections

Use .useOneOf() when exactly one of several sections should render. The first candidate that returns a non-empty string wins:

const hasTasks = section('has-tasks', (ctx: MyContext) => {
  if (ctx.vars.tasks.length === 0) return null;
  return `## Active Tasks\n\n${ctx.vars.tasks.map(t => `- ${t.title}`).join('\n')}`;
});

const noTasks = section('no-tasks', () => '## Active Tasks\n\nNo tasks currently running.');

builder.useOneOf(hasTasks, noTasks);

Builder API

new PromptBuilder<MyContext>()
  .use(section)                      // append (replaces if same id)
  .useOneOf(sectionA, sectionB)      // first match wins
  .group('name', b => b.use(...))    // named group (XML wrapper)
  .without(section)                  // remove by object or string id
  .has(section)                      // check existence
  .ids()                             // list all section ids
  .fork()                            // independent copy
  .build(ctx)                        // render to string
  .build(ctx, { format: 'xml' })     // render with XML tags
  .buildWithMeta(ctx)                // render + { included, excluded }

Testing

import { mockContext, renderSection } from '@anythingai/teleprompt/testing';

// Render a section in isolation
const output = renderSection(webSearch, { flags: { webSearchEnabled: true } });
expect(output).toContain('web search');

// Assert on prompt structure
const { included, excluded } = builder.buildWithMeta(ctx);
expect(included).toContain('web-search');
expect(excluded).toContain('citation');

License

MIT

About

composable prompt building for LLMs

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors