@tstdl/base 0.93.132 → 0.93.134

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.
@@ -2,4 +2,5 @@ export * from './build.js';
2
2
  export * from './format.js';
3
3
  export * from './instructions-formatter.js';
4
4
  export * from './instructions.js';
5
+ export * from './prompt-builder.js';
5
6
  export * from './steering.js';
@@ -2,4 +2,5 @@ export * from './build.js';
2
2
  export * from './format.js';
3
3
  export * from './instructions-formatter.js';
4
4
  export * from './instructions.js';
5
+ export * from './prompt-builder.js';
5
6
  export * from './steering.js';
@@ -35,6 +35,34 @@ function getPrefix(style, index, sectionDepth) {
35
35
  return '';
36
36
  }
37
37
  }
38
+ function dedent(text) {
39
+ const lines = text.split('\n');
40
+ if (lines.length <= 1) {
41
+ return text.trim();
42
+ }
43
+ // Remove leading empty line if any (common in template literals)
44
+ if (lines[0].trim().length == 0) {
45
+ lines.shift();
46
+ }
47
+ // Remove trailing empty line if any
48
+ if (lines.length > 0 && lines[lines.length - 1].trim().length == 0) {
49
+ lines.pop();
50
+ }
51
+ if (lines.length == 0) {
52
+ return '';
53
+ }
54
+ // Find minimum indentation (excluding empty lines)
55
+ const minIndent = lines
56
+ .filter((line) => line.trim().length > 0)
57
+ .reduce((min, line) => {
58
+ const match = /^\s*/.exec(line);
59
+ return Math.min(min, match ? match[0].length : 0);
60
+ }, Infinity);
61
+ if (minIndent == Infinity) {
62
+ return lines.join('\n').trim();
63
+ }
64
+ return lines.map((line) => line.slice(minIndent)).join('\n').trim();
65
+ }
38
66
  /**
39
67
  * Formats a string that might span multiple lines.
40
68
  * It calculates the hanging indent based on the length of the prefix used on the first line.
@@ -63,7 +91,7 @@ function processNode(node, context) {
63
91
  if (isString(node)) {
64
92
  const isSection = context.style == 'sections';
65
93
  const currentBaseIndent = ' '.repeat(isSection ? 0 : context.indentDepth * INDENT_SIZE);
66
- return formatWithHangingIndent(node, currentBaseIndent, '');
94
+ return formatWithHangingIndent(dedent(node), currentBaseIndent, '');
67
95
  }
68
96
  // 1. Unwrap InstructionsList (The Wrapper)
69
97
  // If the node is a wrapper, we adopt its style and instruction, then process its items.
@@ -72,7 +100,7 @@ function processNode(node, context) {
72
100
  // If this Wrapper is a root node, the instruction is usually printed by the caller or ignored.
73
101
  // If this Wrapper is a value of a key, the key printer handles the instruction.
74
102
  // However, if we have a "Root Wrapper" with an instruction (rare in your example), handle it:
75
- const header = node.instruction ? `${node.instruction.trim()}\n\n` : '';
103
+ const header = node.instruction ? `${dedent(node.instruction)}\n\n` : '';
76
104
  // Determine next context
77
105
  const nextContext = {
78
106
  indentDepth: context.indentDepth, // Wrappers don't indent themselves, their content does
@@ -90,7 +118,7 @@ function processNode(node, context) {
90
118
  return node.map((item, index) => {
91
119
  const prefix = getPrefix(context.style, index, context.sectionDepth);
92
120
  if (isString(item)) {
93
- return formatWithHangingIndent(item, currentBaseIndent, prefix);
121
+ return formatWithHangingIndent(dedent(item), currentBaseIndent, prefix);
94
122
  }
95
123
  return processNode(item, context);
96
124
  }).join(separator);
@@ -102,7 +130,7 @@ function processNode(node, context) {
102
130
  // This allows us to pull the wrapper's "instruction" up to the Key line.
103
131
  const isValueWrapper = isInstructionsList(value);
104
132
  const effectiveValue = isValueWrapper ? value.items : value;
105
- const instruction = (isValueWrapper && isDefined(value.instruction)) ? ` ${value.instruction}` : '';
133
+ const instruction = (isValueWrapper && isDefined(value.instruction)) ? ` ${dedent(value.instruction)}` : '';
106
134
  const childStyle = isValueWrapper ? value.style : (isSection ? 'unordered' : 'unordered');
107
135
  // Formatting the Header Line (The Key)
108
136
  let headerLine = '';
@@ -132,14 +160,14 @@ function processNode(node, context) {
132
160
  // or a new paragraph for Sections.
133
161
  if (isSection) {
134
162
  // Section: Header \n\n Content
135
- return `${headerLine}\n\n${effectiveValue.trim()}`;
163
+ return `${headerLine}\n\n${dedent(effectiveValue)}`;
136
164
  }
137
165
  else {
138
166
  // List: "- **Key:** Value"
139
167
  // We need to construct the full string to calculate hanging indent correctly.
140
168
  // headerLine already contains indentation + prefix + key.
141
169
  // We strip the indentation to feed it into the formatting helper effectively.
142
- const fullLine = `${headerLine} ${effectiveValue}`.trim();
170
+ const fullLine = `${headerLine} ${dedent(effectiveValue)}`.trim();
143
171
  return formatWithHangingIndent(fullLine, currentBaseIndent, '');
144
172
  }
145
173
  }
@@ -0,0 +1,24 @@
1
+ import type { Part } from 'genkit';
2
+ import type { ObjectLiteral } from '../../types/index.js';
3
+ import { type Instructions } from './instructions-formatter.js';
4
+ export type PromptBuilderInstructions = Record<string, Instructions>;
5
+ export type PromptBuilderContext = Record<string, PromptBuilderContextItem>;
6
+ export type PromptBuilderContextItem = ObjectLiteral;
7
+ export declare class PromptBuilder {
8
+ #private;
9
+ setSystemRole(role: string): this;
10
+ setRole(role: string): this;
11
+ setSystemTask(task: string): this;
12
+ setTask(task: string): this;
13
+ addSystemMedia(content: Uint8Array, mimeType: string): this;
14
+ addMedia(content: Uint8Array, mimeType: string): this;
15
+ addSystemInstructions(instructions: Record<string, Instructions>): this;
16
+ addInstructions(instructions: Record<string, Instructions>): this;
17
+ addSystemContext(title: string, context: PromptBuilderContextItem): this;
18
+ addSystemContext(context: PromptBuilderContext): this;
19
+ addContext(title: string, context: PromptBuilderContextItem): this;
20
+ addContext(context: PromptBuilderContext): this;
21
+ buildSystemPrompt(): Part[];
22
+ buildUserPrompt(): Part[];
23
+ }
24
+ export declare function promptBuilder(): PromptBuilder;
@@ -0,0 +1,115 @@
1
+ import { encodeBase64 } from '../../utils/base64.js';
2
+ import { fromEntries, objectEntries, objectKeys } from '../../utils/object/index.js';
3
+ import { assertObjectPass, isDefined, isString } from '../../utils/type-guards.js';
4
+ import { formatData } from './format.js';
5
+ import { formatInstructions, sections } from './instructions-formatter.js';
6
+ export class PromptBuilder {
7
+ #systemRole;
8
+ #role;
9
+ #systemTask;
10
+ #task;
11
+ #systemInstructions = {};
12
+ #instructions = {};
13
+ #systemContextParts = {};
14
+ #contextParts = {};
15
+ #systemMedia = [];
16
+ #media = [];
17
+ setSystemRole(role) {
18
+ this.#systemRole = role;
19
+ return this;
20
+ }
21
+ setRole(role) {
22
+ this.#role = role;
23
+ return this;
24
+ }
25
+ setSystemTask(task) {
26
+ this.#systemTask = task;
27
+ return this;
28
+ }
29
+ setTask(task) {
30
+ this.#task = task;
31
+ return this;
32
+ }
33
+ addSystemMedia(content, mimeType) {
34
+ addMedia(content, mimeType, this.#systemMedia);
35
+ return this;
36
+ }
37
+ addMedia(content, mimeType) {
38
+ addMedia(content, mimeType, this.#media);
39
+ return this;
40
+ }
41
+ addSystemInstructions(instructions) {
42
+ this.#systemInstructions = { ...this.#systemInstructions, ...instructions };
43
+ return this;
44
+ }
45
+ addInstructions(instructions) {
46
+ this.#instructions = { ...this.#instructions, ...instructions };
47
+ return this;
48
+ }
49
+ addSystemContext(titleOrContext, contextOrNothing) {
50
+ if ((arguments.length == 1)) {
51
+ this.#systemContextParts = { ...this.#systemContextParts, ...assertObjectPass(titleOrContext) };
52
+ return this;
53
+ }
54
+ this.#systemContextParts[titleOrContext] = contextOrNothing;
55
+ return this;
56
+ }
57
+ addContext(titleOrContext, contextOrNothing) {
58
+ if ((arguments.length == 1)) {
59
+ this.#contextParts = { ...this.#contextParts, ...assertObjectPass(titleOrContext) };
60
+ return this;
61
+ }
62
+ this.#contextParts[titleOrContext] = contextOrNothing;
63
+ return this;
64
+ }
65
+ buildSystemPrompt() {
66
+ return buildPrompt({
67
+ role: this.#systemRole,
68
+ context: this.#systemContextParts,
69
+ instructions: this.#systemInstructions,
70
+ task: this.#systemTask,
71
+ media: this.#systemMedia,
72
+ });
73
+ }
74
+ buildUserPrompt() {
75
+ return buildPrompt({
76
+ role: this.#role,
77
+ context: this.#contextParts,
78
+ instructions: this.#instructions,
79
+ task: this.#task,
80
+ media: this.#media,
81
+ });
82
+ }
83
+ }
84
+ export function promptBuilder() {
85
+ return new PromptBuilder();
86
+ }
87
+ function addMedia(content, mimeType, targetArray) {
88
+ const base64Data = encodeBase64(content);
89
+ const dataUrl = `data:${mimeType};base64,${base64Data}`;
90
+ targetArray.push({ media: { url: dataUrl } });
91
+ }
92
+ function buildPrompt(data) {
93
+ const instructions = {};
94
+ if (isDefined(data.role)) {
95
+ instructions['**Role**'] = data.role;
96
+ }
97
+ if (isDefined(data.context) && objectKeys(data.context).length > 0) {
98
+ const contextEntries = objectEntries(data.context).map(([key, contextPart]) => {
99
+ const string = isString(contextPart) ? contextPart : `\`\`\`toon\n${formatData(contextPart)}\n\`\`\``;
100
+ return [key, string];
101
+ });
102
+ instructions['**Context**'] = sections(fromEntries(contextEntries));
103
+ }
104
+ if (isDefined(data.instructions) && (objectKeys(data.instructions).length > 0)) {
105
+ instructions['**Instructions**'] = data.instructions;
106
+ }
107
+ if (isDefined(data.task)) {
108
+ instructions['**Task**'] = data.task;
109
+ }
110
+ const formattedInstructions = formatInstructions(instructions);
111
+ return [
112
+ ...(data.media ?? []),
113
+ { text: formattedInstructions },
114
+ ];
115
+ }
@@ -0,0 +1 @@
1
+ import '../../polyfills.js';
@@ -0,0 +1,107 @@
1
+ import '../../polyfills.js';
2
+ import { orderedList, promptBuilder, unorderedList } from '../../ai/index.js';
3
+ import { Application } from '../../application/application.js';
4
+ import { provideModule, provideSignalHandler } from '../../application/index.js';
5
+ import { PrettyPrintLogFormatter } from '../../logger/index.js';
6
+ import { provideConsoleLogTransport } from '../../logger/transports/console.js';
7
+ async function main() {
8
+ const builder = promptBuilder();
9
+ // 1. Configure System Instructions
10
+ // System instructions define the persona and fundamental rules of the AI.
11
+ builder
12
+ .setSystemRole('A world-class senior software architect and mentor.')
13
+ .setSystemTask('Review the provided code for architectural flaws, performance bottlenecks, and security vulnerabilities.')
14
+ .addSystemContext('Project Architecture', {
15
+ type: 'Monorepo',
16
+ language: 'TypeScript',
17
+ framework: 'tstdl (Custom Framework)',
18
+ modules: ['API', 'ORM', 'Injector', 'Logger'],
19
+ })
20
+ .addSystemContext('Environment', {
21
+ nodeVersion: process.version,
22
+ platform: process.platform,
23
+ memoryLimit: '4GB',
24
+ })
25
+ .addSystemInstructions({
26
+ 'Output Style': unorderedList('Ensure the output follows these structural rules:', {
27
+ Language: 'Always respond in English.',
28
+ Tone: 'Professional, concise, and constructive.',
29
+ Format: orderedList('Structure your response as follows:', [
30
+ 'Executive Summary',
31
+ 'Detailed Findings (categorized by severity)',
32
+ 'Code Improvements (with examples)',
33
+ 'Next Steps',
34
+ ]),
35
+ }),
36
+ 'Formatting rules': orderedList('Follow these rules for code formatting:', {
37
+ Indentation: 'Use 2 spaces.',
38
+ Semicolons: 'Always use semicolons.',
39
+ Quotes: 'Use single quotes.',
40
+ }),
41
+ 'Response Constraints': unorderedList('Must never include:', [
42
+ 'Personal opinions.',
43
+ 'Speculative performance metrics without data.',
44
+ 'Unnecessary apologies.',
45
+ ]),
46
+ 'Safety Guidelines': unorderedList([
47
+ 'Never suggest disabling security features.',
48
+ 'Avoid recommending deprecated libraries.',
49
+ 'Always prioritize type safety.',
50
+ ]),
51
+ });
52
+ // 2. Configure User Prompt
53
+ // The prompt defines the specific request and context for the current interaction.
54
+ builder
55
+ .setRole('A developer seeking feedback on a new feature.')
56
+ .setTask('Please review this new User Service implementation.')
57
+ .addContext('Code to Review', `
58
+ @Singleton()
59
+ export class UserService extends getRepository(User) {
60
+ readonly #logger = inject(Logger, UserService.name);
61
+
62
+ async createUser(data: UserData): Promise<User> {
63
+ this.#logger.info('Creating user', { data });
64
+ return this.insert(data);
65
+ }
66
+ }
67
+ `)
68
+ .addContext('Requirements', [
69
+ 'Must use private class fields (#).',
70
+ 'Must use the project logger.',
71
+ 'Must extend the base repository.',
72
+ ])
73
+ .addContext('Free Text User Hint', 'Focus on potential issues with dependency injection and logging practices.')
74
+ .addInstructions({
75
+ 'Specific Focus': 'Pay special attention to the use of dependency injection and logging.',
76
+ });
77
+ // 3. Add Media (Simulated)
78
+ // Media can be added (e.g., screenshots of logs or diagrams).
79
+ const dummyImage = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
80
+ builder.addMedia(dummyImage, 'image/png');
81
+ // 4. Build and Display
82
+ const systemMessage = builder.buildSystemPrompt();
83
+ const userMessage = builder.buildUserPrompt();
84
+ console.log('--- SYSTEM MESSAGE ---');
85
+ for (const part of systemMessage) {
86
+ if (part.text) {
87
+ console.log(part.text);
88
+ }
89
+ if (part.media) {
90
+ console.log(`[Media: ${part.media.url}]`);
91
+ }
92
+ }
93
+ console.log('\n\n--- USER PROMPT ---');
94
+ for (const part of userMessage) {
95
+ if (part.text) {
96
+ console.log(part.text);
97
+ }
98
+ if (part.media) {
99
+ console.log(`[Media: ${part.media.url}]`);
100
+ }
101
+ }
102
+ }
103
+ Application.run('PromptBuilder Demo', [
104
+ provideConsoleLogTransport(PrettyPrintLogFormatter),
105
+ provideModule(main),
106
+ provideSignalHandler(),
107
+ ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tstdl/base",
3
- "version": "0.93.132",
3
+ "version": "0.93.134",
4
4
  "author": "Patrick Hein",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -162,8 +162,8 @@
162
162
  "file-type": "^21.3",
163
163
  "genkit": "^1.28",
164
164
  "handlebars": "^4.7",
165
- "@aws-sdk/client-s3": "^3.991",
166
- "@aws-sdk/s3-request-presigner": "^3.991",
165
+ "@aws-sdk/client-s3": "^3.992",
166
+ "@aws-sdk/s3-request-presigner": "^3.992",
167
167
  "mjml": "^4.18",
168
168
  "nodemailer": "^8.0",
169
169
  "pg": "^8.18",
package/test5.js CHANGED
@@ -1,11 +1,14 @@
1
1
  import './polyfills.js';
2
+ import { promptBuilder } from './ai/index.js';
2
3
  import { Application } from './application/application.js';
3
4
  import { provideModule, provideSignalHandler } from './application/index.js';
4
5
  import { PrettyPrintLogFormatter } from './logger/index.js';
5
6
  import { provideConsoleLogTransport } from './logger/transports/console.js';
6
- import { mergePdfs } from './pdf/utils.js';
7
7
  async function main(_cancellationSignal) {
8
- await mergePdfs(['asd', 'bsa']);
8
+ const x = promptBuilder();
9
+ console.log(x.buildSystemPrompt()[0].text);
10
+ console.log('\n\n---\n\n');
11
+ console.log(x.buildUserPrompt()[0].text);
9
12
  }
10
13
  Application.run('Test', [
11
14
  provideConsoleLogTransport(PrettyPrintLogFormatter),