Skip to content
This repository was archived by the owner on Aug 5, 2025. It is now read-only.

Commit e3b86c6

Browse files
authored
feat: implement cache on getPrompt and getPromptById methods (#86)
1 parent d12dd4f commit e3b86c6

File tree

5 files changed

+277
-48
lines changed

5 files changed

+277
-48
lines changed

src/api.ts

Lines changed: 67 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { ReadStream } from 'fs';
55
import { v4 as uuidv4 } from 'uuid';
66

77
import { LiteralClient } from '.';
8+
import { sharedCache } from './cache/sharedcache';
9+
import { getPromptCacheKey } from './cache/utils';
810
import {
911
Dataset,
1012
DatasetExperiment,
@@ -340,6 +342,8 @@ type CreateAttachmentParams = {
340342
* Then you can use the `api` object to make calls to the Literal service.
341343
*/
342344
export class API {
345+
/** @ignore */
346+
private cache: typeof sharedCache;
343347
/** @ignore */
344348
public client: LiteralClient;
345349
/** @ignore */
@@ -372,6 +376,8 @@ export class API {
372376
throw new Error('LITERAL_API_URL not set');
373377
}
374378

379+
this.cache = sharedCache;
380+
375381
this.apiKey = apiKey;
376382
this.url = url;
377383
this.environment = environment;
@@ -399,7 +405,7 @@ export class API {
399405
* @returns The data part of the response from the GraphQL endpoint.
400406
* @throws Will throw an error if the GraphQL call returns errors or if the request fails.
401407
*/
402-
private async makeGqlCall(query: string, variables: any) {
408+
private async makeGqlCall(query: string, variables: any, timeout?: number) {
403409
try {
404410
const response = await axios({
405411
url: this.graphqlEndpoint,
@@ -408,7 +414,8 @@ export class API {
408414
data: {
409415
query: query,
410416
variables: variables
411-
}
417+
},
418+
timeout
412419
});
413420
if (response.data.errors) {
414421
throw new Error(JSON.stringify(response.data.errors));
@@ -2110,41 +2117,75 @@ export class API {
21102117
}
21112118

21122119
/**
2113-
* Retrieves a prompt by its id.
2114-
*
2115-
* @param id ID of the prompt to retrieve.
2116-
* @returns The prompt with given ID.
2120+
* Retrieves a prompt by its id. If the request fails, it will try to get the prompt from the cache.
21172121
*/
21182122
public async getPromptById(id: string) {
21192123
const query = `
2120-
query GetPrompt($id: String!) {
2121-
promptVersion(id: $id) {
2122-
createdAt
2123-
id
2124-
label
2125-
settings
2126-
status
2127-
tags
2128-
templateMessages
2129-
tools
2130-
type
2131-
updatedAt
2132-
url
2133-
variables
2134-
variablesDefaultValues
2135-
version
2136-
lineage {
2137-
name
2124+
query GetPrompt($id: String!) {
2125+
promptVersion(id: $id) {
2126+
createdAt
2127+
id
2128+
label
2129+
settings
2130+
status
2131+
tags
2132+
templateMessages
2133+
tools
2134+
type
2135+
updatedAt
2136+
url
2137+
variables
2138+
variablesDefaultValues
2139+
version
2140+
lineage {
2141+
name
2142+
}
21382143
}
21392144
}
2140-
}
21412145
`;
21422146

21432147
return await this.getPromptWithQuery(query, { id });
21442148
}
21452149

21462150
/**
2147-
* Retrieves a prompt by its name and optionally by its version.
2151+
* Private helper method to execute prompt queries with error handling and caching
2152+
*/
2153+
private async getPromptWithQuery(
2154+
query: string,
2155+
variables: { id?: string; name?: string; version?: number }
2156+
) {
2157+
const cachedPrompt = sharedCache.get(getPromptCacheKey(variables));
2158+
const timeout = cachedPrompt ? 1000 : undefined;
2159+
2160+
try {
2161+
const result = await this.makeGqlCall(query, variables, timeout);
2162+
2163+
if (!result.data || !result.data.promptVersion) {
2164+
return cachedPrompt;
2165+
}
2166+
2167+
const promptData = result.data.promptVersion;
2168+
promptData.provider = promptData.settings?.provider;
2169+
promptData.name = promptData.lineage?.name;
2170+
delete promptData.lineage;
2171+
if (promptData.settings) {
2172+
delete promptData.settings.provider;
2173+
}
2174+
2175+
const prompt = new Prompt(this, promptData);
2176+
2177+
sharedCache.put(prompt.id, prompt);
2178+
sharedCache.put(prompt.name, prompt);
2179+
sharedCache.put(`${prompt.name}:${prompt.version}`, prompt);
2180+
2181+
return prompt;
2182+
} catch (error) {
2183+
return cachedPrompt;
2184+
}
2185+
}
2186+
2187+
/**
2188+
* Retrieves a prompt by its name and optionally by its version. If the request fails, it will try to get the prompt from the cache.
21482189
*
21492190
* @param name - The name of the prompt to retrieve.
21502191
* @param version - The version number of the prompt (optional).
@@ -2171,31 +2212,9 @@ export class API {
21712212
}
21722213
}
21732214
`;
2174-
21752215
return await this.getPromptWithQuery(query, { name, version });
21762216
}
21772217

2178-
private async getPromptWithQuery(
2179-
query: string,
2180-
variables: Record<string, any>
2181-
) {
2182-
const result = await this.makeGqlCall(query, variables);
2183-
2184-
if (!result.data || !result.data.promptVersion) {
2185-
return null;
2186-
}
2187-
2188-
const promptData = result.data.promptVersion;
2189-
promptData.provider = promptData.settings?.provider;
2190-
promptData.name = promptData.lineage?.name;
2191-
delete promptData.lineage;
2192-
if (promptData.settings) {
2193-
delete promptData.settings.provider;
2194-
}
2195-
2196-
return new Prompt(this, promptData);
2197-
}
2198-
21992218
/**
22002219
* Retrieves a prompt A/B testing rollout by its name.
22012220
*

src/cache/sharedcache.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const cache: Map<string, any> = new Map();
2+
3+
class SharedCache {
4+
private static instance: SharedCache;
5+
6+
public constructor() {
7+
if (SharedCache.instance) {
8+
throw new Error('SharedCache can only be created once');
9+
}
10+
SharedCache.instance = this;
11+
}
12+
13+
public getInstance(): SharedCache {
14+
return this;
15+
}
16+
17+
public getCache(): Map<string, any> {
18+
return cache;
19+
}
20+
21+
public get(key: string): any {
22+
return cache.get(key);
23+
}
24+
25+
public put(key: string, value: any): void {
26+
cache.set(key, value);
27+
}
28+
29+
public clear(): void {
30+
cache.clear();
31+
}
32+
}
33+
34+
export const sharedCache = new SharedCache();
35+
36+
export default sharedCache;

src/cache/utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export function getPromptCacheKey({
2+
id,
3+
name,
4+
version
5+
}: {
6+
id?: string;
7+
name?: string;
8+
version?: number;
9+
}): string {
10+
if (id) {
11+
return id;
12+
} else if (name && typeof version === 'number') {
13+
return `${name}:${version}`;
14+
} else if (name) {
15+
return name;
16+
}
17+
throw new Error('Either id or name must be provided');
18+
}

tests/api.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import axios from 'axios';
12
import 'dotenv/config';
23
import { v4 as uuidv4 } from 'uuid';
34

45
import { ChatGeneration, IGenerationMessage, LiteralClient } from '../src';
6+
import { sharedCache } from '../src/cache/sharedcache';
57
import { Dataset } from '../src/evaluation/dataset';
68
import { Score } from '../src/evaluation/score';
9+
import { Prompt, PromptConstructor } from '../src/prompt-engineering/prompt';
710
import { sleep } from './utils';
811

912
describe('End to end tests for the SDK', function () {
@@ -597,6 +600,30 @@ describe('End to end tests for the SDK', function () {
597600
});
598601

599602
describe('Prompt api', () => {
603+
const mockPromptData: PromptConstructor = {
604+
id: 'test-id',
605+
name: 'test-prompt',
606+
version: 1,
607+
createdAt: new Date().toISOString(),
608+
type: 'CHAT',
609+
templateMessages: [],
610+
tools: [],
611+
settings: {
612+
provider: 'test',
613+
model: 'test',
614+
frequency_penalty: 0,
615+
presence_penalty: 0,
616+
temperature: 0,
617+
top_p: 0,
618+
max_tokens: 0
619+
},
620+
variables: [],
621+
variablesDefaultValues: {},
622+
metadata: {},
623+
items: [],
624+
provider: 'test'
625+
};
626+
600627
it('should get a prompt by name', async () => {
601628
const prompt = await client.api.getPrompt('Default');
602629

@@ -657,6 +684,56 @@ is a templated list.`;
657684
expect(formatted[0].content).toBe(expected);
658685
});
659686

687+
it('should fallback to cache when getPromptById DB call fails', async () => {
688+
const prompt = new Prompt(client.api, mockPromptData);
689+
sharedCache.put(prompt.id, prompt);
690+
sharedCache.put(prompt.name, prompt);
691+
sharedCache.put(`${prompt.name}:${prompt.version}`, prompt);
692+
693+
jest
694+
.spyOn(client.api as any, 'makeGqlCall')
695+
.mockRejectedValueOnce(new Error('DB Error'));
696+
697+
const result = await client.api.getPromptById(prompt.id);
698+
expect(result).toEqual(prompt);
699+
});
700+
701+
it('should fallback to cache when getPrompt DB call fails', async () => {
702+
const prompt = new Prompt(client.api, mockPromptData);
703+
704+
sharedCache.put(prompt.id, prompt);
705+
sharedCache.put(prompt.name, prompt);
706+
sharedCache.put(`${prompt.name}:${prompt.version}`, prompt);
707+
708+
jest.spyOn(axios, 'post').mockRejectedValueOnce(new Error('DB Error'));
709+
710+
const result = await client.api.getPrompt(prompt.id);
711+
expect(result).toEqual(prompt);
712+
});
713+
714+
it('should update cache with fresh data on successful DB call', async () => {
715+
const prompt = new Prompt(client.api, mockPromptData);
716+
717+
jest.spyOn(client.api as any, 'makeGqlCall').mockResolvedValueOnce({
718+
data: { promptVersion: prompt }
719+
});
720+
721+
await client.api.getPromptById(prompt.id);
722+
723+
const cachedPrompt = sharedCache.get(prompt.id);
724+
expect(cachedPrompt).toBeDefined();
725+
expect(cachedPrompt?.id).toBe(prompt.id);
726+
});
727+
728+
it('should return null when both DB and cache fail', async () => {
729+
jest
730+
.spyOn(client.api as any, 'makeGqlCall')
731+
.mockRejectedValueOnce(new Error('DB Error'));
732+
733+
const result = await client.api.getPromptById('non-existent-id');
734+
expect(result).toBeUndefined();
735+
});
736+
660737
it('should get a prompt A/B testing configuration', async () => {
661738
const promptName = 'TypeScript SDK E2E Tests';
662739

0 commit comments

Comments
 (0)