@joshuapassos/prompt-ts 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # prompt-ts
2
+
3
+ Type-safe prompt templating engine for TypeScript. Build structured LLM prompts with compile-time placeholder validation, multi-language support, and optional Zod schema integration.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add prompt-ts
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - **Type-safe placeholders** — `{{key}}` syntax validated at compile time
14
+ - **System/User message pairs** — structured prompt rendering for chat-based LLMs
15
+ - **Multi-language support** — define templates per language, select at render time
16
+ - **Composable sections** — reusable prompt fragments via `promptSection()`
17
+ - **Zod schema** — optional structured output validation
18
+ - **Conditional options** — only requires `systemOptions`/`userOptions` when the corresponding template has placeholders
19
+
20
+ ## Usage
21
+
22
+ ### Basic (string mode)
23
+
24
+ ```ts
25
+ import { prompt } from "prompt-ts";
26
+
27
+ const p = prompt(
28
+ "greet",
29
+ "You are a {{role}}." as const,
30
+ "Say hello to {{name}}." as const
31
+ );
32
+
33
+ const result = p.render({
34
+ systemOptions: { role: "assistant" },
35
+ userOptions: { name: "Alice" },
36
+ });
37
+ // => { systemPrompt: "You are a assistant.", userPrompt: "Say hello to Alice." }
38
+ ```
39
+
40
+ ### Multi-language mode
41
+
42
+ ```ts
43
+ const p = prompt(
44
+ "classify",
45
+ {
46
+ en: "You are a {{category}} classifier" as const,
47
+ pt: "Você é um classificador de {{categoria}}" as const,
48
+ },
49
+ {
50
+ en: "Classify: {{text}}" as const,
51
+ pt: "Classifique: {{texto}}" as const,
52
+ }
53
+ );
54
+
55
+ p.render("en", {
56
+ systemOptions: { category: "sentiment" },
57
+ userOptions: { text: "I'm happy" },
58
+ });
59
+
60
+ p.render("pt", {
61
+ systemOptions: { categoria: "sentimentos" },
62
+ userOptions: { texto: "Estou feliz" },
63
+ });
64
+ ```
65
+
66
+ ### Composable sections
67
+
68
+ ```ts
69
+ import { promptSection } from "prompt-ts";
70
+
71
+ const persona = promptSection("You are a {{role}}." as const);
72
+ const tone = promptSection("Be professional and concise." as const);
73
+
74
+ const systemPrompt = [
75
+ persona.render({ role: "translator" }),
76
+ tone.render(),
77
+ ].join("\n\n");
78
+ // => "You are a translator.\n\nBe professional and concise."
79
+ ```
80
+
81
+ Sections can be composed into a `prompt`:
82
+
83
+ ```ts
84
+ const p = prompt(
85
+ "translate",
86
+ systemPrompt,
87
+ "Translate: {{text}}" as const
88
+ );
89
+
90
+ p.render({ userOptions: { text: "Hello world" } });
91
+ ```
92
+
93
+ ### With Zod schema
94
+
95
+ ```ts
96
+ import { z } from "zod";
97
+
98
+ const schema = z.object({
99
+ sentiment: z.enum(["positive", "negative", "neutral"]),
100
+ confidence: z.number(),
101
+ });
102
+
103
+ const p = prompt(
104
+ "sentiment",
105
+ "You are a sentiment classifier." as const,
106
+ "Classify: {{text}}" as const,
107
+ schema
108
+ );
109
+
110
+ // Access the schema for structured output parsing
111
+ p.zodSchema; // z.ZodObject<...>
112
+ ```
113
+
114
+ ## API
115
+
116
+ ### `prompt(name, systemTemplate, userTemplate, zodSchema?)`
117
+
118
+ | Param | Type | Description |
119
+ |---|---|---|
120
+ | `name` | `string` | Unique identifier for logging/tracing |
121
+ | `systemTemplate` | `string \| Record<string, string>` | System message template(s) |
122
+ | `userTemplate` | `string \| Record<string, string>` | User message template(s) |
123
+ | `zodSchema` | `z.ZodType<T>` | Optional Zod schema for output validation |
124
+
125
+ #### `.render(options)` (string mode)
126
+
127
+ #### `.render(lang, options)` (multi-language mode)
128
+
129
+ Returns `{ systemPrompt: string, userPrompt: string }`.
130
+
131
+ Options are `{ systemOptions?, userOptions? }` — each is required only when the corresponding template contains `{{placeholders}}`.
132
+
133
+ ### `promptSection(template)`
134
+
135
+ Creates a reusable prompt fragment. Returns `{ template, render(vars?) }`.
136
+
137
+ ## Scripts
138
+
139
+ ```bash
140
+ pnpm test # Run tests
141
+ pnpm build # Compile TypeScript
142
+ pnpm typecheck # Type-check without emitting
143
+ ```
@@ -0,0 +1,71 @@
1
+ import type z from "zod";
2
+ type ExtractNewTypes<S extends string> = S extends `${string}{{${infer Key}}}${infer Rest}` ? [Trim<Key>, ...ExtractNewTypes<Rest>] : [];
3
+ type Separator = " ";
4
+ type Trim<T extends string, Acc extends string = ""> = T extends `${infer Char}${infer Rest}` ? Char extends Separator ? Trim<Rest, Acc> : Trim<Rest, `${Acc}${Char}`> : Acc;
5
+ export type ArrayToObject<T extends string[]> = {
6
+ [K in T[number]]: string | number | boolean;
7
+ };
8
+ type IsEmpty<T extends unknown[]> = T extends [] ? true : false;
9
+ type OptionsField<Keys extends string[]> = IsEmpty<Keys> extends true ? Record<string, never> : ArrayToObject<Keys>;
10
+ type OptionalField<Keys extends string[]> = IsEmpty<Keys> extends true ? true : false;
11
+ type EvalOptions<STemplate extends string, UTemplate extends string> = {
12
+ [K in "systemOptions" as OptionalField<ExtractNewTypes<STemplate>> extends true ? never : K]: OptionsField<ExtractNewTypes<STemplate>>;
13
+ } & {
14
+ [K in "userOptions" as OptionalField<ExtractNewTypes<UTemplate>> extends true ? never : K]: OptionsField<ExtractNewTypes<UTemplate>>;
15
+ } & {
16
+ [K in "systemOptions" as OptionalField<ExtractNewTypes<STemplate>> extends true ? K : never]?: OptionsField<ExtractNewTypes<STemplate>>;
17
+ } & {
18
+ [K in "userOptions" as OptionalField<ExtractNewTypes<UTemplate>> extends true ? K : never]?: OptionsField<ExtractNewTypes<UTemplate>>;
19
+ };
20
+ type HasAnyPlaceholders<STemplate extends string, UTemplate extends string> = [
21
+ IsEmpty<ExtractNewTypes<STemplate>>,
22
+ IsEmpty<ExtractNewTypes<UTemplate>>
23
+ ] extends [true, true] ? false : true;
24
+ type EvalArgs<STemplate extends string, UTemplate extends string> = HasAnyPlaceholders<STemplate, UTemplate> extends false ? [options?: EvalOptions<STemplate, UTemplate>] : [options: EvalOptions<STemplate, UTemplate>];
25
+ export type PromptTemplate = string | Record<string, string>;
26
+ type MatchLanguages<S extends PromptTemplate> = S extends string ? PromptTemplate : Record<keyof S & string, string>;
27
+ type NormalizeTemplate<T extends PromptTemplate> = T extends string ? {
28
+ default: T;
29
+ } : T;
30
+ type GetTemplate<T extends PromptTemplate, K extends string> = T extends string ? T : K extends keyof T ? T[K] : never;
31
+ type StringTemplate<T extends PromptTemplate, K extends string> = GetTemplate<T, K> extends string ? GetTemplate<T, K> : never;
32
+ type SectionArgs<T extends string> = IsEmpty<ExtractNewTypes<T>> extends true ? [vars?: Record<string, never>] : [vars: ArrayToObject<ExtractNewTypes<T>>];
33
+ /**
34
+ * Creates a reusable, type-safe prompt fragment with `{{placeholder}}` interpolation.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const persona = promptSection("You are a {{role}}." as const);
39
+ * persona.render({ role: "translator" }); // "You are a translator."
40
+ * ```
41
+ */
42
+ export declare function promptSection<T extends string>(template: T): {
43
+ template: T;
44
+ render(...args: SectionArgs<T>): string;
45
+ };
46
+ /**
47
+ * Type-safe prompt template with system/user message pairs and optional Zod schema for structured output.
48
+ *
49
+ * Supports two modes:
50
+ * - **String mode** — pass plain strings as templates, call `render()` without a language key.
51
+ * - **Multi-language mode** — pass `Record<string, string>` maps, call `render(lang, options)`.
52
+ *
53
+ * Placeholders use the `{{key}}` syntax and are fully type-checked at compile time.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * const p = prompt("greet", "Hello {{name}}" as const, "Hi!" as const);
58
+ * p.render({ systemOptions: { name: "Alice" } });
59
+ * // => { systemPrompt: "Hello Alice", userPrompt: "Hi!" }
60
+ * ```
61
+ */
62
+ export declare function prompt<S extends PromptTemplate, U extends MatchLanguages<S>, T = unknown>(promptName: string, systemPrompt: S, userPrompt: U, zodSchema?: z.ZodType<T>): {
63
+ promptName: string;
64
+ zodSchema: z.ZodType<T, z.ZodTypeDef, T> | undefined;
65
+ render<TLang extends keyof NormalizeTemplate<S> & keyof NormalizeTemplate<U> = keyof NormalizeTemplate<S> & keyof NormalizeTemplate<U>>(...args: [S, U] extends [string, string] ? EvalArgs<S extends string ? S : never, U extends string ? U : never> : [lang: TLang, ...EvalArgs<StringTemplate<S, TLang & string>, StringTemplate<U, TLang & string>>]): {
66
+ systemPrompt: string;
67
+ userPrompt: string;
68
+ };
69
+ };
70
+ export {};
71
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,65 @@
1
+ function replacePlaceholders(template, vars) {
2
+ return template.replaceAll(/\{\{([^}]+)\}\}/g, (match, rawKey) => {
3
+ const key = rawKey.trim();
4
+ return vars && key in vars ? String(vars[key]) : match;
5
+ });
6
+ }
7
+ /**
8
+ * Creates a reusable, type-safe prompt fragment with `{{placeholder}}` interpolation.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const persona = promptSection("You are a {{role}}." as const);
13
+ * persona.render({ role: "translator" }); // "You are a translator."
14
+ * ```
15
+ */
16
+ export function promptSection(template) {
17
+ return {
18
+ template,
19
+ render(...args) {
20
+ return replacePlaceholders(template, args[0]);
21
+ },
22
+ };
23
+ }
24
+ /**
25
+ * Type-safe prompt template with system/user message pairs and optional Zod schema for structured output.
26
+ *
27
+ * Supports two modes:
28
+ * - **String mode** — pass plain strings as templates, call `render()` without a language key.
29
+ * - **Multi-language mode** — pass `Record<string, string>` maps, call `render(lang, options)`.
30
+ *
31
+ * Placeholders use the `{{key}}` syntax and are fully type-checked at compile time.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const p = prompt("greet", "Hello {{name}}" as const, "Hi!" as const);
36
+ * p.render({ systemOptions: { name: "Alice" } });
37
+ * // => { systemPrompt: "Hello Alice", userPrompt: "Hi!" }
38
+ * ```
39
+ */
40
+ export function prompt(promptName, systemPrompt, userPrompt, zodSchema) {
41
+ const normalizedSystem = (typeof systemPrompt === "string" ? { default: systemPrompt } : systemPrompt);
42
+ const normalizedUser = (typeof userPrompt === "string" ? { default: userPrompt } : userPrompt);
43
+ return {
44
+ promptName,
45
+ zodSchema,
46
+ render(...args) {
47
+ let lang;
48
+ let evalOptions;
49
+ if (typeof args[0] === "string") {
50
+ lang = args[0];
51
+ evalOptions = (args[1] ?? {});
52
+ }
53
+ else {
54
+ lang = "default";
55
+ evalOptions = (args[0] ?? {});
56
+ }
57
+ const systemOptions = evalOptions.systemOptions;
58
+ const userOptions = evalOptions.userOptions;
59
+ const sys = replacePlaceholders(normalizedSystem[lang], systemOptions);
60
+ const usr = replacePlaceholders(normalizedUser[lang], userOptions);
61
+ return { systemPrompt: sys, userPrompt: usr };
62
+ },
63
+ };
64
+ }
65
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAoEA,SAAS,mBAAmB,CAAC,QAAgB,EAAE,IAAyC;IACtF,OAAO,QAAQ,CAAC,UAAU,CAAC,kBAAkB,EAAE,CAAC,KAAK,EAAE,MAAc,EAAE,EAAE;QACvE,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;QAC1B,OAAO,IAAI,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACzD,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAmB,QAAW;IACzD,OAAO;QACL,QAAQ;QACR,MAAM,CAAC,GAAG,IAAoB;YAC5B,OAAO,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAwC,CAAC,CAAC;QACvF,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,MAAM,CACpB,UAAkB,EAClB,YAAe,EACf,UAAa,EACb,SAAwB;IAExB,MAAM,gBAAgB,GAAG,CACvB,OAAO,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,YAAY,CACpD,CAAC;IAE1B,MAAM,cAAc,GAAG,CACrB,OAAO,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,UAAU,CAC9C,CAAC;IAE1B,OAAO;QACL,UAAU;QACV,SAAS;QACT,MAAM,CAIJ,GAAG,IAEiG;YAEpG,IAAI,IAAY,CAAC;YACjB,IAAI,WAAoC,CAAC;YAEzC,IAAI,OAAO,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,EAAE,CAAC;gBAChC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAW,CAAC;gBACzB,WAAW,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAA4B,CAAC;YAC3D,CAAC;iBAAM,CAAC;gBACN,IAAI,GAAG,SAAS,CAAC;gBACjB,WAAW,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAA4B,CAAC;YAC3D,CAAC;YAED,MAAM,aAAa,GAAG,WAAW,CAAC,aAAoD,CAAC;YACvF,MAAM,WAAW,GAAG,WAAW,CAAC,WAAkD,CAAC;YAEnF,MAAM,GAAG,GAAG,mBAAmB,CAAE,gBAA2C,CAAC,IAAI,CAAW,EAAE,aAAa,CAAC,CAAC;YAE7G,MAAM,GAAG,GAAG,mBAAmB,CAAE,cAAyC,CAAC,IAAI,CAAW,EAAE,WAAW,CAAC,CAAC;YAEzG,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC;QAChD,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@joshuapassos/prompt-ts",
3
+ "version": "1.0.0",
4
+ "description": "Type-safe prompt templating engine with Zod schema support",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
+ "type": "module",
8
+ "author": "Joshua Passos",
9
+ "dependencies": {
10
+ "zod": "^3.25.32"
11
+ },
12
+ "devDependencies": {
13
+ "@biomejs/biome": "^2.4.9",
14
+ "knip": "^6.0.6",
15
+ "typescript": "~6.0.2",
16
+ "vitest": "^4.1.2"
17
+ },
18
+ "files": [
19
+ "dist/**"
20
+ ],
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "require": "./dist/index.js",
25
+ "import": "./dist/index.js",
26
+ "default": "./dist/index.js"
27
+ }
28
+ },
29
+ "scripts": {
30
+ "build": "tsc",
31
+ "typecheck": "tsc --noEmit",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest",
34
+ "clean": "rm -rf dist",
35
+ "lint": "biome check --write .",
36
+ "lint:check": "biome check .",
37
+ "knip": "knip"
38
+ }
39
+ }
package/src/index.ts ADDED
@@ -0,0 +1,156 @@
1
+ import type z from "zod";
2
+
3
+ type ExtractNewTypes<S extends string> = S extends `${string}{{${infer Key}}}${infer Rest}`
4
+ ? [Trim<Key>, ...ExtractNewTypes<Rest>]
5
+ : [];
6
+
7
+ type Separator = " ";
8
+
9
+ type Trim<T extends string, Acc extends string = ""> = T extends `${infer Char}${infer Rest}`
10
+ ? Char extends Separator
11
+ ? Trim<Rest, Acc>
12
+ : Trim<Rest, `${Acc}${Char}`>
13
+ : Acc;
14
+
15
+ export type ArrayToObject<T extends string[]> = {
16
+ [K in T[number]]: string | number | boolean;
17
+ };
18
+
19
+ type IsEmpty<T extends unknown[]> = T extends [] ? true : false;
20
+
21
+ type OptionsField<Keys extends string[]> = IsEmpty<Keys> extends true ? Record<string, never> : ArrayToObject<Keys>;
22
+
23
+ type OptionalField<Keys extends string[]> = IsEmpty<Keys> extends true ? true : false;
24
+
25
+ type EvalOptions<STemplate extends string, UTemplate extends string> = {
26
+ [K in "systemOptions" as OptionalField<ExtractNewTypes<STemplate>> extends true ? never : K]: OptionsField<
27
+ ExtractNewTypes<STemplate>
28
+ >;
29
+ } & {
30
+ [K in "userOptions" as OptionalField<ExtractNewTypes<UTemplate>> extends true ? never : K]: OptionsField<
31
+ ExtractNewTypes<UTemplate>
32
+ >;
33
+ } & {
34
+ [K in "systemOptions" as OptionalField<ExtractNewTypes<STemplate>> extends true ? K : never]?: OptionsField<
35
+ ExtractNewTypes<STemplate>
36
+ >;
37
+ } & {
38
+ [K in "userOptions" as OptionalField<ExtractNewTypes<UTemplate>> extends true ? K : never]?: OptionsField<
39
+ ExtractNewTypes<UTemplate>
40
+ >;
41
+ };
42
+
43
+ type HasAnyPlaceholders<STemplate extends string, UTemplate extends string> = [
44
+ IsEmpty<ExtractNewTypes<STemplate>>,
45
+ IsEmpty<ExtractNewTypes<UTemplate>>,
46
+ ] extends [true, true]
47
+ ? false
48
+ : true;
49
+
50
+ type EvalArgs<STemplate extends string, UTemplate extends string> =
51
+ HasAnyPlaceholders<STemplate, UTemplate> extends false
52
+ ? [options?: EvalOptions<STemplate, UTemplate>]
53
+ : [options: EvalOptions<STemplate, UTemplate>];
54
+
55
+ export type PromptTemplate = string | Record<string, string>;
56
+
57
+ type MatchLanguages<S extends PromptTemplate> = S extends string ? PromptTemplate : Record<keyof S & string, string>;
58
+
59
+ type NormalizeTemplate<T extends PromptTemplate> = T extends string ? { default: T } : T;
60
+
61
+ type GetTemplate<T extends PromptTemplate, K extends string> = T extends string ? T : K extends keyof T ? T[K] : never;
62
+
63
+ type StringTemplate<T extends PromptTemplate, K extends string> =
64
+ GetTemplate<T, K> extends string ? GetTemplate<T, K> : never;
65
+
66
+ type SectionArgs<T extends string> =
67
+ IsEmpty<ExtractNewTypes<T>> extends true ? [vars?: Record<string, never>] : [vars: ArrayToObject<ExtractNewTypes<T>>];
68
+
69
+ function replacePlaceholders(template: string, vars: Record<string, unknown> | undefined) {
70
+ return template.replaceAll(/\{\{([^}]+)\}\}/g, (match, rawKey: string) => {
71
+ const key = rawKey.trim();
72
+ return vars && key in vars ? String(vars[key]) : match;
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Creates a reusable, type-safe prompt fragment with `{{placeholder}}` interpolation.
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * const persona = promptSection("You are a {{role}}." as const);
82
+ * persona.render({ role: "translator" }); // "You are a translator."
83
+ * ```
84
+ */
85
+ export function promptSection<T extends string>(template: T) {
86
+ return {
87
+ template,
88
+ render(...args: SectionArgs<T>) {
89
+ return replacePlaceholders(template, args[0] as Record<string, unknown> | undefined);
90
+ },
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Type-safe prompt template with system/user message pairs and optional Zod schema for structured output.
96
+ *
97
+ * Supports two modes:
98
+ * - **String mode** — pass plain strings as templates, call `render()` without a language key.
99
+ * - **Multi-language mode** — pass `Record<string, string>` maps, call `render(lang, options)`.
100
+ *
101
+ * Placeholders use the `{{key}}` syntax and are fully type-checked at compile time.
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * const p = prompt("greet", "Hello {{name}}" as const, "Hi!" as const);
106
+ * p.render({ systemOptions: { name: "Alice" } });
107
+ * // => { systemPrompt: "Hello Alice", userPrompt: "Hi!" }
108
+ * ```
109
+ */
110
+ export function prompt<S extends PromptTemplate, U extends MatchLanguages<S>, T = unknown>(
111
+ promptName: string,
112
+ systemPrompt: S,
113
+ userPrompt: U,
114
+ zodSchema?: z.ZodType<T>,
115
+ ) {
116
+ const normalizedSystem = (
117
+ typeof systemPrompt === "string" ? { default: systemPrompt } : systemPrompt
118
+ ) as NormalizeTemplate<S>;
119
+
120
+ const normalizedUser = (
121
+ typeof userPrompt === "string" ? { default: userPrompt } : userPrompt
122
+ ) as NormalizeTemplate<U>;
123
+
124
+ return {
125
+ promptName,
126
+ zodSchema,
127
+ render<
128
+ TLang extends keyof NormalizeTemplate<S> & keyof NormalizeTemplate<U> = keyof NormalizeTemplate<S> &
129
+ keyof NormalizeTemplate<U>,
130
+ >(
131
+ ...args: [S, U] extends [string, string]
132
+ ? EvalArgs<S extends string ? S : never, U extends string ? U : never>
133
+ : [lang: TLang, ...EvalArgs<StringTemplate<S, TLang & string>, StringTemplate<U, TLang & string>>]
134
+ ) {
135
+ let lang: string;
136
+ let evalOptions: Record<string, unknown>;
137
+
138
+ if (typeof args[0] === "string") {
139
+ lang = args[0] as string;
140
+ evalOptions = (args[1] ?? {}) as Record<string, unknown>;
141
+ } else {
142
+ lang = "default";
143
+ evalOptions = (args[0] ?? {}) as Record<string, unknown>;
144
+ }
145
+
146
+ const systemOptions = evalOptions.systemOptions as Record<string, unknown> | undefined;
147
+ const userOptions = evalOptions.userOptions as Record<string, unknown> | undefined;
148
+
149
+ const sys = replacePlaceholders((normalizedSystem as Record<string, string>)[lang] as string, systemOptions);
150
+
151
+ const usr = replacePlaceholders((normalizedUser as Record<string, string>)[lang] as string, userOptions);
152
+
153
+ return { systemPrompt: sys, userPrompt: usr };
154
+ },
155
+ };
156
+ }