From 29207be8a79d9f3f6db6e316f1ff11ce77f10fb0 Mon Sep 17 00:00:00 2001 From: Philipp Burckhardt Date: Fri, 26 May 2023 14:28:27 -0400 Subject: [PATCH] feat: add initial event logging implementation --- package.json | 4 ++ pnpm-lock.yaml | 33 +++++++++++- src/logging.ts | 139 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 src/logging.ts diff --git a/package.json b/package.json index fa87d13..89d1d53 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@inquirer/checkbox": "^1.3.0", "@inquirer/editor": "^1.1.0", "@inquirer/select": "^1.2.0", + "debug": "^4.3.4", "handlebars": "^4.7.7", "js-tiktoken": "^1.0.6", "jsonrepair": "^3.1.0", @@ -52,6 +53,7 @@ "p-timeout": "^6.1.1", "quick-lru": "^6.1.1", "ts-dedent": "^2.2.0", + "uuid": "^9.0.0", "zod": "^3.21.4", "zod-to-ts": "^1.1.4", "zod-validation-error": "^1.3.0" @@ -59,8 +61,10 @@ "devDependencies": { "@keyv/redis": "^2.5.8", "@trivago/prettier-plugin-sort-imports": "^4.1.1", + "@types/debug": "^4.1.8", "@types/node": "^20.2.5", "@types/sinon": "^10.0.15", + "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^5.59.8", "@typescript-eslint/parser": "^5.59.8", "ava": "^5.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a9ae17..8d78002 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: '@inquirer/select': specifier: ^1.2.0 version: 1.2.0 + debug: + specifier: ^4.3.4 + version: 4.3.4 handlebars: specifier: ^4.7.7 version: 4.7.7 @@ -47,6 +50,9 @@ dependencies: ts-dedent: specifier: ^2.2.0 version: 2.2.0 + uuid: + specifier: ^9.0.0 + version: 9.0.0 zod: specifier: ^3.21.4 version: 3.21.4 @@ -64,12 +70,18 @@ devDependencies: '@trivago/prettier-plugin-sort-imports': specifier: ^4.1.1 version: 4.1.1(prettier@2.8.8) + '@types/debug': + specifier: ^4.1.8 + version: 4.1.8 '@types/node': specifier: ^20.2.5 version: 20.2.5 '@types/sinon': specifier: ^10.0.15 version: 10.0.15 + '@types/uuid': + specifier: ^9.0.1 + version: 9.0.1 '@typescript-eslint/eslint-plugin': specifier: ^5.59.8 version: 5.59.8(@typescript-eslint/parser@5.59.8)(eslint@8.41.0)(typescript@5.0.4) @@ -715,6 +727,12 @@ packages: - supports-color dev: true + /@types/debug@4.1.8: + resolution: {integrity: sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==} + dependencies: + '@types/ms': 0.7.31 + dev: true + /@types/json-schema@7.0.12: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} dev: true @@ -723,6 +741,10 @@ packages: resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} dev: true + /@types/ms@0.7.31: + resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} + dev: true + /@types/node@20.2.5: resolution: {integrity: sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==} dev: true @@ -749,6 +771,10 @@ packages: resolution: {integrity: sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==} dev: true + /@types/uuid@9.0.1: + resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==} + dev: true + /@typescript-eslint/eslint-plugin@5.59.8(@typescript-eslint/parser@5.59.8)(eslint@8.41.0)(typescript@5.0.4): resolution: {integrity: sha512-JDMOmhXteJ4WVKOiHXGCoB96ADWg9q7efPWHRViT/f09bA8XOMLAVHHju3l0MkZnG1izaWXYmgvQcUjTRcpShQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1409,7 +1435,6 @@ packages: optional: true dependencies: ms: 2.1.2 - dev: true /decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} @@ -2762,7 +2787,6 @@ packages: /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3927,6 +3951,11 @@ packages: punycode: 2.3.0 dev: true + /uuid@9.0.0: + resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} + hasBin: true + dev: false + /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: diff --git a/src/logging.ts b/src/logging.ts new file mode 100644 index 0000000..6893c08 --- /dev/null +++ b/src/logging.ts @@ -0,0 +1,139 @@ +import logger from 'debug' +import { v4 as uuidv4 } from 'uuid' + +/** + * Type of events that can occur within the library. + */ +export enum EventType { + LLM_CALL = 'LLM_CALL', + LLM_COMPLETION = 'LLM_COMPLETION' +} + +/** + * Severity levels of an event. + */ +export enum EventSeverity { + DEBUG, + INFO, + WARNING, + ERROR, + CRITICAL +} + +/* + * Define minimum LOG_LEVEL, defaulting to EventSeverity.INFO if not provided or if an invalid value is provided. Any events below that level won't be logged to the console. + */ +let LOG_LEVEL: EventSeverity = EventSeverity.INFO +if ( + process.env.DEBUG_LOG_LEVEL && + EventSeverity[process.env.DEBUG_LOG_LEVEL.toUpperCase()] !== undefined +) { + LOG_LEVEL = EventSeverity[process.env.DEBUG_LOG_LEVEL.toUpperCase()] +} else if (process.env.DEBUG_LOG_LEVEL) { + throw new Error(`Invalid value for LOG_LEVEL: ${process.env.DEBUG_LOG_LEVEL}`) +} + +/** + * Define loggers for each severity level such that logs can be filtered by severity. + */ +const LOGGERS: Record> = { + [EventSeverity.CRITICAL]: logger('agentic:events:critical'), + [EventSeverity.ERROR]: logger('agentic:events:error'), + [EventSeverity.WARNING]: logger('agentic:events:warning'), + [EventSeverity.INFO]: logger('agentic:events:info'), + [EventSeverity.DEBUG]: logger('agentic:events:debug') +} + +/** + * Payload of an event. + */ +interface EventPayload { + [key: string]: unknown +} + +/** + * Data required to create a new Event object. + */ +interface EventData { + parentId?: string + id?: string + timestamp?: Date + payload?: EventPayload + severity?: EventSeverity + version?: number +} + +/** + * Events that occur within the library (should be treated as immutable). + */ +export class Event { + public readonly type: EventType + public readonly parentId?: string + public readonly id: string + public readonly timestamp: Date + public readonly payload?: EventPayload + public readonly severity?: EventSeverity + public readonly version: number + + constructor(type: EventType, data: EventData = {}) { + this.type = type + this.parentId = data.parentId ?? null + this.id = data.id ?? uuidv4() + this.timestamp = data.timestamp ?? new Date() + this.payload = data.payload ? { ...data.payload } : {} // Only doing a shallow instead of a deep copy for performance reasons.. + this.severity = data.severity ?? EventSeverity.INFO + this.version = data.version ?? 1 // Default to version 1 if not provided... + } + + /** + * Converts a JSON string representation of an event back into an Event object. + */ + static fromJSON(json: string): Event { + const { type, ...data } = JSON.parse(json) + + // Convert the timestamp back into a Date object, since `JSON.parse()` will have turned it into a string: + data.timestamp = new Date(data.timestamp) + const event = new Event(type, data) + return event + } + + /** + * Converts the event to a JSON string representation. + * + * @returns JSON representation + */ + toJSON(): string { + return JSON.stringify({ + type: this.type, + parentId: this.parentId, + id: this.id, + timestamp: this.timestamp.toISOString(), + payload: this.payload, + severity: this.severity, + version: this.version + }) + } + + /** + * Converts the event to a human-readable string representation suitable for logging. + * + * @returns string representation + */ + toString(): string { + return `Event { type: ${this.type}, parentId: ${this.parentId}, id: ${ + this.id + }, timestamp: ${this.timestamp.toISOString()}, payload: ${JSON.stringify( + this.payload + )}, severity: ${this.severity} }` + } + + /** + * Logs the event to the console. + */ + log(): void { + if (this.severity >= LOG_LEVEL) { + const logger = LOGGERS[this.severity] + logger(this.toString()) + } + } +}