kbot cache object level

This commit is contained in:
lovebird 2025-04-07 11:26:54 +02:00
parent ad216c888d
commit 243d8c2fad
5 changed files with 209 additions and 112 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,10 @@
import { IKBotTask } from '@polymech/ai-tools';
import { AsyncTransformer, ErrorCallback, FilterCallback } from './async-iterator.js';
export interface ILogger {
info: (message: string) => void;
warn: (message: string) => void;
error: (message: string, error?: any) => void;
}
export declare const removeEmptyObjects: (obj: any) => any;
export interface FieldMapping {
jsonPath: string;
@ -17,12 +22,7 @@ export interface CacheConfig {
namespace?: string;
expiration?: number;
}
export declare function createLLMTransformer(options: IKBotTask, logger?: {
info: (message: string) => void;
warn: (message: string) => void;
error: (message: string, error?: any) => void;
}, cacheConfig?: CacheConfig): AsyncTransformer;
export declare function createIterator(obj: Record<string, any>, optionsMixin: Partial<IKBotTask>, globalOptions?: {
export interface IOptions {
throttleDelay?: number;
concurrentTasks?: number;
errorCallback?: ErrorCallback;
@ -30,11 +30,11 @@ export declare function createIterator(obj: Record<string, any>, optionsMixin: P
transformerFactory?: (options: IKBotTask) => AsyncTransformer;
maxRetries?: number;
retryDelay?: number;
logger?: {
error: (message: string, error?: any) => void;
};
logger?: ILogger;
cacheConfig?: CacheConfig;
}): IteratorFactory;
}
export declare function createLLMTransformer(options: IKBotTask, logger?: ILogger, cacheConfig?: CacheConfig): AsyncTransformer;
export declare function createIterator(obj: Record<string, any>, optionsMixin: Partial<IKBotTask>, globalOptions?: IOptions): IteratorFactory;
export declare function transformWithMappings(obj: Record<string, any>, createTransformer: (options: IKBotTask) => AsyncTransformer, mappings: FieldMapping[], globalOptions?: {
throttleDelay?: number;
concurrentTasks?: number;

File diff suppressed because one or more lines are too long

View File

@ -49,21 +49,21 @@ const exampleData = {
// Field mappings definition
const fieldMappings: FieldMapping[] = [
{
jsonPath: '$..description',
jsonPath: '$.products.fruits[*].description',
targetPath: null,
options: {
prompt: 'Make this description more engaging and detailed, around 20-30 words'
}
},
{
jsonPath: '$..nutrition',
jsonPath: '$.products.fruits[*].details.nutrition',
targetPath: null,
options: {
prompt: 'Expand this nutrition information with 2-3 specific health benefits, around 25-35 words'
}
},
{
jsonPath: '$..name',
jsonPath: '$.products.fruits[*].name',
targetPath: 'marketingName',
options: {
prompt: 'Generate a more appealing marketing name for this product'
@ -89,17 +89,6 @@ export async function factoryExample() {
console.log("========================================");
try {
// Clear the test-data folder
const outputPath = path.resolve('./tests/test-data/core/iterator-factory-data.json');
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Make sure the file is empty to start with a clean slate
if (fs.existsSync(outputPath)) {
fs.unlinkSync(outputPath);
}
const data = JSON.parse(JSON.stringify(exampleData));
// Global options for all transformations
@ -120,7 +109,7 @@ export async function factoryExample() {
errorCallback,
filterCallback: async () => true,
transformerFactory: (options) => createLLMTransformer(options, logger, cacheConfig),
logger: logger,
logger,
cacheConfig
}
);
@ -129,15 +118,25 @@ export async function factoryExample() {
console.log("First run - should transform and cache results:");
await iterator.transform(fieldMappings);
const outputPath = path.resolve('./tests/test-data/core/iterator-factory-data.json');
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Make sure the file is empty to start with a clean slate
if (fs.existsSync(outputPath)) {
fs.unlinkSync(outputPath);
}
write(outputPath, JSON.stringify(data, null, 2));
console.log("Transformation complete. Results saved to", outputPath);
// Print the transformed data structure
console.log("\nTransformed data structure:");
console.log(JSON.stringify(data.products.fruits[0], null, 2).substring(0, 200) + "...");
console.log(JSON.stringify(data.products.fruits[0], null, 2));
// Create a second instance with the same data to test cache
console.log("\n\n========================================");
console.log("\n========================================");
console.log("Second run - should use cached results:");
console.log("========================================");
@ -152,7 +151,7 @@ export async function factoryExample() {
errorCallback,
filterCallback: async () => true,
transformerFactory: (options) => createLLMTransformer(options, logger, cacheConfig),
logger: logger,
logger,
cacheConfig
}
);
@ -162,13 +161,9 @@ export async function factoryExample() {
console.log("\nBefore/After Comparison Example:");
console.log(`Original description: ${exampleData.products.fruits[0].description}`);
console.log(`Transformed description: ${data.products.fruits[0].description}`);
console.log(`Transformed description: ${data2.products.fruits[0].description}`);
console.log(`Original name: ${exampleData.products.fruits[0].name}`);
// Check if name is an object with marketingName property
const name = data.products.fruits[0].name;
const marketingName = typeof name === 'object' && name !== null ? name.marketingName : 'Not available';
console.log(`New marketing name: ${marketingName}`);
console.log(`Marketing name: ${data2.products.fruits[0].marketingName || 'Not available'}`);
return data;
} catch (error) {

View File

@ -2,6 +2,19 @@ import { IKBotTask } from '@polymech/ai-tools'
import { AsyncTransformer, ErrorCallback, FilterCallback, defaultError, defaultFilters, testFilters, transformObjectWithOptions } from './async-iterator.js'
import { run } from './commands/run.js'
import { get_cached_object, set_cached_object, rm_cached_object } from "@polymech/cache"
import { deepClone } from "@polymech/core/objects"
export interface ILogger {
info: (message: string) => void;
warn: (message: string) => void;
error: (message: string, error?: any) => void;
}
const dummyLogger: ILogger = {
info: () => {},
warn: () => {},
error: () => {}
};
export const removeEmptyObjects = (obj: any): any => {
if (obj === null || obj === undefined) return obj;
@ -39,6 +52,18 @@ export interface CacheConfig {
expiration?: number; // in seconds
}
export interface IOptions {
throttleDelay?: number;
concurrentTasks?: number;
errorCallback?: ErrorCallback;
filterCallback?: FilterCallback;
transformerFactory?: (options: IKBotTask) => AsyncTransformer;
maxRetries?: number;
retryDelay?: number;
logger?: ILogger;
cacheConfig?: CacheConfig;
}
const DEFAULT_CACHE_CONFIG: Required<CacheConfig> = {
enabled: true,
namespace: 'llm-responses',
@ -47,21 +72,15 @@ const DEFAULT_CACHE_CONFIG: Required<CacheConfig> = {
export function createLLMTransformer(
options: IKBotTask,
logger?: {
info: (message: string) => void;
warn: (message: string) => void;
error: (message: string, error?: any) => void;
},
logger: ILogger = dummyLogger,
cacheConfig?: CacheConfig
): AsyncTransformer {
const config: Required<CacheConfig> = { ...DEFAULT_CACHE_CONFIG, ...cacheConfig };
return async (input: string, jsonPath: string): Promise<string> => {
if (logger) {
logger.info(`Transforming field at path: ${jsonPath}`);
logger.info(`Input: ${input}`);
logger.info(`Using prompt: ${options.prompt}`);
}
logger.info(`Transforming field at path: ${jsonPath}`);
logger.info(`Input: ${input}`);
logger.info(`Using prompt: ${options.prompt}`);
const kbotTask: IKBotTask = {
...options,
@ -81,7 +100,7 @@ export function createLLMTransformer(
if (config.enabled) {
const cachedResponse = await get_cached_object({ ca_options: cacheKeyObj }, config.namespace) as { content: string };
if (cachedResponse?.content) {
if (logger) logger.info(`Using cached LLM response for prompt: ${kbotTask.prompt.substring(0, 100)}...`);
logger.info(`Using cached LLM response for prompt: ${kbotTask.prompt.substring(0, 100)}...`);
return cachedResponse.content;
}
}
@ -97,21 +116,17 @@ export function createLLMTransformer(
{ content: result },
{ expiration: config.expiration }
);
if (logger) logger.info(`Cached LLM response for prompt: ${kbotTask.prompt.substring(0, 100)}...`);
logger.info(`Cached LLM response for prompt: ${kbotTask.prompt.substring(0, 100)}...`);
}
if (logger) logger.info(`Result: ${result}`);
logger.info(`Result: ${result}`);
return result;
}
if (logger) logger.warn(`No valid result received for ${jsonPath}, returning original`);
logger.warn(`No valid result received for ${jsonPath}, returning original`);
return input;
} catch (error) {
if (logger) {
logger.error(`Error calling LLM API: ${error.message}`, error);
} else {
console.error(`Error calling LLM API: ${error.message}`, error);
}
logger.error(`Error calling LLM API: ${error.message}`, error);
return input;
}
};
@ -120,17 +135,7 @@ export function createLLMTransformer(
export function createIterator(
obj: Record<string, any>,
optionsMixin: Partial<IKBotTask>,
globalOptions: {
throttleDelay?: number
concurrentTasks?: number
errorCallback?: ErrorCallback
filterCallback?: FilterCallback
transformerFactory?: (options: IKBotTask) => AsyncTransformer
maxRetries?: number
retryDelay?: number
logger?: { error: (message: string, error?: any) => void }
cacheConfig?: CacheConfig
} = {}
globalOptions: IOptions = {}
): IteratorFactory {
const {
throttleDelay = 1000,
@ -140,26 +145,71 @@ export function createIterator(
transformerFactory,
maxRetries = 3,
retryDelay = 2000,
logger,
logger = dummyLogger,
cacheConfig
} = globalOptions;
const config: Required<CacheConfig> = { ...DEFAULT_CACHE_CONFIG, ...cacheConfig };
const defaultTransformerFactory = (options: IKBotTask): AsyncTransformer => {
return async (input: string): Promise<string> => input;
};
const createTransformer = transformerFactory || defaultTransformerFactory;
const createObjectCacheKey = (data: Record<string, any>, mappings: FieldMapping[]) => {
return removeEmptyObjects({
data: JSON.stringify(data),
mappings: mappings.map(m => ({
jsonPath: m.jsonPath,
targetPath: m.targetPath,
options: {
model: optionsMixin.model,
router: optionsMixin.router,
mode: optionsMixin.mode,
prompt: m.options?.prompt
}
}))
});
};
const deepMerge = (target: Record<string, any>, source: Record<string, any>) => {
for (const key in source) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
if (!target[key]) target[key] = {};
deepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
};
return {
createTransformer,
transform: async (mappings: FieldMapping[]): Promise<void> => {
if (config.enabled) {
const objectCacheKey = createObjectCacheKey(obj, mappings);
const cachedObject = await get_cached_object(
{ ca_options: objectCacheKey },
'transformed-objects'
) as { content: Record<string, any> };
if (cachedObject?.content) {
logger.info('Using cached transformed object');
deepMerge(obj, cachedObject.content);
return;
}
}
// If no cache hit or caching disabled, perform the transformations
const transformedObj = JSON.parse(JSON.stringify(obj));
for (const mapping of mappings) {
const mergedOptions = { ...optionsMixin, ...mapping.options } as IKBotTask;
const { jsonPath, targetPath = null, maxRetries: mappingRetries, retryDelay: mappingRetryDelay } = mapping;
const transformer = createTransformer(mergedOptions);
await transformObjectWithOptions(
obj,
transformedObj,
transformer,
{
jsonPath,
@ -173,6 +223,21 @@ export function createIterator(
}
);
}
// Cache the transformed object
if (config.enabled) {
const objectCacheKey = createObjectCacheKey(obj, mappings);
await set_cached_object(
{ ca_options: objectCacheKey },
'transformed-objects',
{ content: transformedObj },
{ expiration: config.expiration }
);
logger.info('Cached transformed object');
}
// Apply the transformations to the original object
deepMerge(obj, transformedObj);
}
};
}