mirror of
https://github.com/FJurmanovic/sugarr.git
synced 2026-11-11 15:51:09 +00:00
Add cached Langfuse PromptService
This commit is contained in:
10
src/core/langfuse/PromptGateway.ts
Normal file
10
src/core/langfuse/PromptGateway.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export interface RawPrompt {
|
||||||
|
name: string;
|
||||||
|
version: number;
|
||||||
|
config: Record<string, unknown>;
|
||||||
|
compile(variables: Record<string, unknown>): { role: 'system' | 'user' | 'assistant'; content: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PromptFetcher {
|
||||||
|
fetch(promptName: string): Promise<RawPrompt>;
|
||||||
|
}
|
||||||
46
src/core/langfuse/PromptService.ts
Normal file
46
src/core/langfuse/PromptService.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import type { PromptFetcher, RawPrompt } from './PromptGateway';
|
||||||
|
import type { CompiledPrompt } from './types';
|
||||||
|
|
||||||
|
interface CacheEntry {
|
||||||
|
raw: RawPrompt;
|
||||||
|
fetchedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PromptService {
|
||||||
|
private readonly cache = new Map<string, CacheEntry>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly fetcher: PromptFetcher,
|
||||||
|
private readonly cacheEnabled: boolean
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getCompiled(promptName: string, variables: Record<string, unknown>): Promise<CompiledPrompt> {
|
||||||
|
const raw = await this.getRaw(promptName);
|
||||||
|
return {
|
||||||
|
name: raw.name,
|
||||||
|
version: raw.version,
|
||||||
|
config: raw.config,
|
||||||
|
messages: raw.compile(variables),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidate(promptName?: string): void {
|
||||||
|
if (promptName) {
|
||||||
|
this.cache.delete(promptName);
|
||||||
|
} else {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getRaw(promptName: string): Promise<RawPrompt> {
|
||||||
|
if (this.cacheEnabled) {
|
||||||
|
const cached = this.cache.get(promptName);
|
||||||
|
if (cached) return cached.raw;
|
||||||
|
}
|
||||||
|
const raw = await this.fetcher.fetch(promptName);
|
||||||
|
if (this.cacheEnabled) {
|
||||||
|
this.cache.set(promptName, { raw, fetchedAt: Date.now() });
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/core/langfuse/types.ts
Normal file
6
src/core/langfuse/types.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface CompiledPrompt {
|
||||||
|
name: string;
|
||||||
|
version: number;
|
||||||
|
config: Record<string, unknown>;
|
||||||
|
messages: { role: 'system' | 'user' | 'assistant'; content: string }[];
|
||||||
|
}
|
||||||
64
test/core/langfuse/PromptService.test.ts
Normal file
64
test/core/langfuse/PromptService.test.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { PromptService } from '../../../src/core/langfuse/PromptService';
|
||||||
|
import type { PromptFetcher, RawPrompt } from '../../../src/core/langfuse/PromptGateway';
|
||||||
|
|
||||||
|
function makeRawPrompt(name: string, version = 1): RawPrompt {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
config: { model: 'gpt-4o-mini' },
|
||||||
|
compile: (variables) => [{ role: 'user', content: `hello ${String(variables.who)}` }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('PromptService', () => {
|
||||||
|
it('fetches once and reuses the cache on subsequent calls when caching is enabled', async () => {
|
||||||
|
const fetch = vi.fn().mockResolvedValue(makeRawPrompt('greet'));
|
||||||
|
const fetcher: PromptFetcher = { fetch };
|
||||||
|
const service = new PromptService(fetcher, true);
|
||||||
|
|
||||||
|
await service.getCompiled('greet', { who: 'a' });
|
||||||
|
const compiled = await service.getCompiled('greet', { who: 'b' });
|
||||||
|
|
||||||
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
|
expect(compiled.messages).toEqual([{ role: 'user', content: 'hello b' }]);
|
||||||
|
expect(compiled.config).toEqual({ model: 'gpt-4o-mini' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('always refetches when caching is disabled', async () => {
|
||||||
|
const fetch = vi.fn().mockResolvedValue(makeRawPrompt('greet'));
|
||||||
|
const fetcher: PromptFetcher = { fetch };
|
||||||
|
const service = new PromptService(fetcher, false);
|
||||||
|
|
||||||
|
await service.getCompiled('greet', { who: 'a' });
|
||||||
|
await service.getCompiled('greet', { who: 'b' });
|
||||||
|
|
||||||
|
expect(fetch).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invalidate(name) forces a refetch for that name only', async () => {
|
||||||
|
const fetch = vi.fn().mockResolvedValue(makeRawPrompt('greet'));
|
||||||
|
const fetcher: PromptFetcher = { fetch };
|
||||||
|
const service = new PromptService(fetcher, true);
|
||||||
|
|
||||||
|
await service.getCompiled('greet', {});
|
||||||
|
service.invalidate('greet');
|
||||||
|
await service.getCompiled('greet', {});
|
||||||
|
|
||||||
|
expect(fetch).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invalidate() with no name clears the whole cache', async () => {
|
||||||
|
const fetch = vi.fn().mockImplementation((name: string) => Promise.resolve(makeRawPrompt(name)));
|
||||||
|
const fetcher: PromptFetcher = { fetch };
|
||||||
|
const service = new PromptService(fetcher, true);
|
||||||
|
|
||||||
|
await service.getCompiled('a', {});
|
||||||
|
await service.getCompiled('b', {});
|
||||||
|
service.invalidate();
|
||||||
|
await service.getCompiled('a', {});
|
||||||
|
await service.getCompiled('b', {});
|
||||||
|
|
||||||
|
expect(fetch).toHaveBeenCalledTimes(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user