mirror of
https://github.com/Kaelio/ktx.git
synced 2026-07-13 11:22:11 +02:00
Initial open-source release
This commit is contained in:
commit
1a42152e6f
1199 changed files with 257054 additions and 0 deletions
2
packages/context/src/prompts/index.ts
Normal file
2
packages/context/src/prompts/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export type { PromptContext, PromptServiceOptions } from './prompt.service.js';
|
||||
export { PromptService } from './prompt.service.js';
|
||||
54
packages/context/src/prompts/prompt.service.test.ts
Normal file
54
packages/context/src/prompts/prompt.service.test.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { PromptService } from './prompt.service.js';
|
||||
|
||||
describe('PromptService', () => {
|
||||
let dir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
dir = await mkdtemp(join(tmpdir(), 'klo-prompts-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('loads prompt files from the configured prompt directory', async () => {
|
||||
await writeFile(join(dir, 'hello.md'), 'Hello {{name}}', 'utf-8');
|
||||
const service = new PromptService({ promptsDir: dir, partials: [] });
|
||||
|
||||
await expect(service.loadPrompt('hello')).resolves.toBe('Hello {{name}}');
|
||||
});
|
||||
|
||||
it('loads prompts from additional directories when the primary directory misses', async () => {
|
||||
const extraDir = await mkdtemp(join(tmpdir(), 'klo-prompts-extra-'));
|
||||
try {
|
||||
await writeFile(join(extraDir, 'memory_agent_research.md'), '<role>Packaged memory prompt</role>', 'utf-8');
|
||||
const service = new PromptService({ promptsDir: dir, additionalPromptDirs: [extraDir], partials: [] });
|
||||
|
||||
await expect(service.loadPrompt('memory_agent_research')).resolves.toBe(
|
||||
'<role>Packaged memory prompt</role>',
|
||||
);
|
||||
} finally {
|
||||
await rm(extraDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('formats prompts with default settings and context settings', async () => {
|
||||
await writeFile(join(dir, 'settings.md'), '{{settings.flag}} {{settings.mode}} {{name}}', 'utf-8');
|
||||
const service = new PromptService({
|
||||
promptsDir: dir,
|
||||
partials: [],
|
||||
defaultSettings: { flag: true, mode: 'default' },
|
||||
});
|
||||
|
||||
const rendered = await service.formatPrompt('settings', {
|
||||
name: 'Ada',
|
||||
settings: { mode: 'override' },
|
||||
});
|
||||
|
||||
expect(rendered).toBe('true override Ada');
|
||||
});
|
||||
});
|
||||
108
packages/context/src/prompts/prompt.service.ts
Normal file
108
packages/context/src/prompts/prompt.service.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import Handlebars from 'handlebars';
|
||||
import { type KloLogger, noopLogger } from '../core/index.js';
|
||||
|
||||
export interface PromptContext {
|
||||
current_date?: string;
|
||||
business_rules?: string;
|
||||
datasource_description?: string;
|
||||
tables_and_columns_summary?: string;
|
||||
metadata?: string;
|
||||
settings?: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface PromptServiceOptions {
|
||||
promptsDir: string;
|
||||
additionalPromptDirs?: string[];
|
||||
defaultSettings?: Record<string, unknown>;
|
||||
partials?: string[];
|
||||
logger?: KloLogger;
|
||||
}
|
||||
|
||||
export class PromptService {
|
||||
private readonly logger: KloLogger;
|
||||
private readonly partials: string[];
|
||||
private partialsRegistered = false;
|
||||
|
||||
constructor(private readonly options: PromptServiceOptions) {
|
||||
this.logger = options.logger ?? noopLogger;
|
||||
this.partials = options.partials ?? ['clinical_policy'];
|
||||
Handlebars.registerHelper('eq', (a: unknown, b: unknown) => a === b);
|
||||
Handlebars.registerHelper('json', (context: unknown) => JSON.stringify(context, null, 2));
|
||||
Handlebars.registerHelper('truncate', (str: string, len: number) =>
|
||||
typeof str === 'string' && str.length > len ? `${str.substring(0, len)}...` : str,
|
||||
);
|
||||
Handlebars.registerHelper('addOne', (index: number) => index + 1);
|
||||
this.logger.log(`Prompt service initialized with directory: ${options.promptsDir}`);
|
||||
}
|
||||
|
||||
private promptDirs(): string[] {
|
||||
return [this.options.promptsDir, ...(this.options.additionalPromptDirs ?? [])];
|
||||
}
|
||||
|
||||
private async ensurePartials(): Promise<void> {
|
||||
if (this.partialsRegistered) {
|
||||
return;
|
||||
}
|
||||
for (const name of this.partials) {
|
||||
let registered = false;
|
||||
for (const promptsDir of this.promptDirs()) {
|
||||
try {
|
||||
const content = await readFile(join(promptsDir, `${name}.md`), 'utf-8');
|
||||
Handlebars.registerPartial(name, content);
|
||||
registered = true;
|
||||
break;
|
||||
} catch {}
|
||||
}
|
||||
if (!registered) {
|
||||
this.logger.warn(`Could not register ${name} partial`);
|
||||
}
|
||||
}
|
||||
this.partialsRegistered = true;
|
||||
}
|
||||
|
||||
async loadPrompt(promptName: string, extension = 'md'): Promise<string> {
|
||||
const tried: string[] = [];
|
||||
for (const promptsDir of this.promptDirs()) {
|
||||
const promptFile = join(promptsDir, `${promptName}.${extension}`);
|
||||
tried.push(promptFile);
|
||||
try {
|
||||
const content = await readFile(promptFile, 'utf-8');
|
||||
this.logger.debug(`Loaded prompt template: ${promptName}.${extension}`);
|
||||
return content;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const paths = tried.join(', ');
|
||||
this.logger.error(`Prompt file not found: ${paths}`);
|
||||
throw new Error(`Prompt file not found in any configured directory: ${paths}`);
|
||||
}
|
||||
|
||||
async formatPrompt(promptName: string, context: PromptContext): Promise<string> {
|
||||
await this.ensurePartials();
|
||||
try {
|
||||
const fullContext: PromptContext = {
|
||||
current_date: context.current_date || new Date().toISOString().split('T')[0],
|
||||
business_rules: context.business_rules || '',
|
||||
...context,
|
||||
settings: {
|
||||
...this.options.defaultSettings,
|
||||
...context.settings,
|
||||
},
|
||||
};
|
||||
|
||||
const templateSource = await this.loadPrompt(promptName);
|
||||
const template = Handlebars.compile(templateSource, { noEscape: true });
|
||||
const rendered = template(fullContext);
|
||||
|
||||
this.logger.debug(`Formatted prompt: ${promptName} (${rendered.length} chars)`);
|
||||
return rendered;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
this.logger.error(`Error formatting prompt ${promptName}: ${errorMessage}`);
|
||||
throw new Error(`Failed to format prompt ${promptName}: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue