diff --git a/.eslintrc b/.eslintrc index 56dd0274..5a20710f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,13 +1,7 @@ { "parser": "@typescript-eslint/parser", - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended" - ], - "plugins": [ - "@typescript-eslint", - "eslint-plugin-tsdoc" - ], + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + "plugins": ["@typescript-eslint", "eslint-plugin-tsdoc"], "root": true, "env": { "browser": false, @@ -38,47 +32,39 @@ "error", { "blankLine": "always", - "prev": [ - "block", - "block-like" - ], + "prev": ["block", "block-like"], "next": "*" }, { "blankLine": "always", "prev": "*", - "next": [ - "block", - "block-like" - ] + "next": ["block", "block-like"] }, { "blankLine": "any", - "prev": [ - "singleline-const", - "singleline-let", - "expression" - ], - "next": [ - "block", - "block-like" - ] + "prev": ["singleline-const", "singleline-let", "expression"], + "next": ["block", "block-like"] } ], "tsdoc/syntax": "warn" }, "overrides": [ { - "files": [ - "*.tsx" - ], + "files": ["*.tsx"], "rules": { "no-undef": "off" } + }, + { + // disable process.env for framework source files + "files": "src/**/*.ts", + "rules": { + "no-process-env": "error" + } } ], "settings": {}, "globals": { "__DEV__": true } -} \ No newline at end of file +} diff --git a/src/agentic.ts b/src/agentic.ts index 08165afc..5f242961 100644 --- a/src/agentic.ts +++ b/src/agentic.ts @@ -10,7 +10,6 @@ import { defaultLogger } from './logger' import { defaultIDGeneratorFn, isFunction, isString } from './utils' export class Agentic { - // _taskMap: WeakMap> protected _ky: types.KyInstance protected _logger: types.Logger @@ -103,7 +102,7 @@ export class Agentic { return this._idGeneratorFn } - openaiChatCompletion( + openaiChatCompletion( promptOrChatCompletionParams: | types.ChatMessageContent | SetOptional, 'model'>, @@ -148,7 +147,7 @@ export class Agentic { /** * Shortcut for creating an OpenAI chat completion call with the `gpt-3.5-turbo` model. */ - gpt3( + gpt3( promptOrChatCompletionParams: | types.ChatMessageContent | SetOptional, 'model'>, @@ -194,7 +193,7 @@ export class Agentic { /** * Shortcut for creating an OpenAI chat completion call with the `gpt-4` model. */ - gpt4( + gpt4( promptOrChatCompletionParams: | types.ChatMessageContent | SetOptional, 'model'>, diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 00000000..92c61230 --- /dev/null +++ b/src/env.ts @@ -0,0 +1,15 @@ +export function getEnv( + name: string, + defaultValue?: string +): string | undefined { + try { + return ( + (typeof process !== 'undefined' + ? // eslint-disable-next-line no-process-env + process.env?.[name] + : undefined) ?? defaultValue + ) + } catch (e) { + return defaultValue + } +} diff --git a/src/logger.ts b/src/logger.ts index fb41972b..6741cdd6 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,6 +1,8 @@ import { pino } from 'pino' import pinoPretty from 'pino-pretty' +import { getEnv } from './env' + export type Level = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' export type LevelWithSilent = Level | 'silent' @@ -103,7 +105,7 @@ export interface Logger { export const defaultLogger: Logger = pino( { - level: process.env.LOG_LEVEL || 'info' + level: getEnv('LOG_LEVEL', 'info') }, pinoPretty({ sync: true, diff --git a/src/logging.ts b/src/logging.ts index b84d15b2..e38b5727 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,6 +1,8 @@ import logger from 'debug' import { v4 as uuidv4 } from 'uuid' +import { getEnv } from './env' + /** * List of events that can occur within the library. */ @@ -26,14 +28,12 @@ type SeverityType = (typeof Severity)[keyof typeof Severity] * Define minimum LOG_LEVEL, defaulting to Severity.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: SeverityType = Severity.INFO +const logLevelEnv = getEnv('DEBUG_LOG_LEVEL') -if ( - process.env.DEBUG_LOG_LEVEL && - Severity[process.env.DEBUG_LOG_LEVEL.toUpperCase()] !== undefined -) { - LOG_LEVEL = Severity[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}`) +if (logLevelEnv && Severity[logLevelEnv.toUpperCase()] !== undefined) { + LOG_LEVEL = Severity[logLevelEnv.toUpperCase()] +} else if (logLevelEnv) { + throw new Error(`Invalid value for LOG_LEVEL: ${logLevelEnv}`) } /** diff --git a/src/services/bing-web-search.ts b/src/services/bing-web-search.ts index 1b7b5338..ffd576a4 100644 --- a/src/services/bing-web-search.ts +++ b/src/services/bing-web-search.ts @@ -1,5 +1,7 @@ import defaultKy from 'ky' +import { getEnv } from '@/env' + export const BING_API_BASE_URL = 'https://api.bing.microsoft.com' export interface BingWebSearchQuery { @@ -241,7 +243,7 @@ export class BingWebSearchClient { apiBaseUrl: string constructor({ - apiKey = process.env.BING_API_KEY, + apiKey = getEnv('BING_API_KEY'), apiBaseUrl = BING_API_BASE_URL, ky = defaultKy }: { diff --git a/src/services/diffbot.ts b/src/services/diffbot.ts index efd30eba..a6cd428c 100644 --- a/src/services/diffbot.ts +++ b/src/services/diffbot.ts @@ -2,6 +2,7 @@ import defaultKy from 'ky' import { AbortError } from 'p-retry' import pThrottle from 'p-throttle' +import { getEnv } from '@/env' import { throttleKy } from '@/utils' export const DIFFBOT_API_BASE_URL = 'https://api.diffbot.com' @@ -334,7 +335,7 @@ export class DiffbotClient { apiKnowledgeGraphBaseUrl: string constructor({ - apiKey = process.env.DIFFBOT_API_KEY, + apiKey = getEnv('DIFFBOT_API_KEY'), apiBaseUrl = DIFFBOT_API_BASE_URL, apiKnowledgeGraphBaseUrl = DIFFBOT_KNOWLEDGE_GRAPH_API_BASE_URL, timeoutMs = 30_000, diff --git a/src/services/metaphor.ts b/src/services/metaphor.ts index efd8e5f7..d079e2db 100644 --- a/src/services/metaphor.ts +++ b/src/services/metaphor.ts @@ -1,5 +1,7 @@ import defaultKy from 'ky' +import { getEnv } from '@/env' + export const METAPHOR_API_BASE_URL = 'https://api.metaphor.systems' /** @@ -119,7 +121,7 @@ export class MetaphorClient { readonly apiBaseUrl: string constructor({ - apiKey = process.env.METAPHOR_API_KEY, + apiKey = getEnv('METAPHOR_API_KEY'), apiBaseUrl = METAPHOR_API_BASE_URL, ky = defaultKy }: { diff --git a/src/services/novu.ts b/src/services/novu.ts index da627944..17d6c653 100644 --- a/src/services/novu.ts +++ b/src/services/novu.ts @@ -1,5 +1,7 @@ import defaultKy from 'ky' +import { getEnv } from '@/env' + /** * Base URL endpoint for the Novu API. */ @@ -110,7 +112,7 @@ export class NovuClient { * Novu API client constructor. */ constructor({ - apiKey = process.env.NOVU_API_KEY, + apiKey = getEnv('NOVU_API_KEY'), apiBaseUrl = NOVU_API_BASE_URL, ky = defaultKy }: { diff --git a/src/services/serpapi.ts b/src/services/serpapi.ts index a74624fc..00a6946f 100644 --- a/src/services/serpapi.ts +++ b/src/services/serpapi.ts @@ -1,5 +1,7 @@ import defaultKy from 'ky' +import { getEnv } from '@/env' + /** * All types have been exported from the `serpapi` package, which we're * not using directly because it is bloated and has compatibility issues. @@ -642,7 +644,7 @@ export class SerpAPIClient { params: Partial constructor({ - apiKey = process.env.SERPAPI_API_KEY ?? process.env.SERP_API_KEY, + apiKey = getEnv('SERPAPI_API_KEY') ?? getEnv('SERP_API_KEY'), apiBaseUrl = SERPAPI_BASE_URL, ky = defaultKy, ...params diff --git a/src/services/slack.ts b/src/services/slack.ts index 62e8aee6..d205212f 100644 --- a/src/services/slack.ts +++ b/src/services/slack.ts @@ -1,5 +1,6 @@ import defaultKy from 'ky' +import { getEnv } from '@/env' import { sleep } from '@/utils' export const SLACK_API_BASE_URL = 'https://slack.com/api' @@ -261,9 +262,9 @@ export class SlackClient { protected defaultChannel?: string constructor({ - apiKey = process.env.SLACK_API_KEY, + apiKey = getEnv('SLACK_API_KEY'), baseUrl = SLACK_API_BASE_URL, - defaultChannel = process.env.SLACK_DEFAULT_CHANNEL, + defaultChannel = getEnv('SLACK_DEFAULT_CHANNEL'), ky = defaultKy }: { apiKey?: string diff --git a/src/services/twilio-conversation.ts b/src/services/twilio-conversation.ts index 8239df81..e88b0c6b 100644 --- a/src/services/twilio-conversation.ts +++ b/src/services/twilio-conversation.ts @@ -1,6 +1,7 @@ import defaultKy from 'ky' import { DEFAULT_BOT_NAME } from '@/constants' +import { getEnv } from '@/env' import { chunkMultipleStrings, chunkString, sleep } from '@/utils' export const TWILIO_CONVERSATION_API_BASE_URL = @@ -208,11 +209,12 @@ export class TwilioConversationClient { defaultRecipientPhoneNumber?: string constructor({ - accountSid = process.env.TWILIO_ACCOUNT_SID, - authToken = process.env.TWILIO_AUTH_TOKEN, - phoneNumber = process.env.TWILIO_PHONE_NUMBER, - defaultRecipientPhoneNumber = process.env - .TWILIO_DEFAULT_RECIPIENT_PHONE_NUMBER, + accountSid = getEnv('TWILIO_ACCOUNT_SID'), + authToken = getEnv('TWILIO_AUTH_TOKEN'), + phoneNumber = getEnv('TWILIO_PHONE_NUMBER'), + defaultRecipientPhoneNumber = getEnv( + 'TWILIO_DEFAULT_RECIPIENT_PHONE_NUMBER' + ), apiBaseUrl = TWILIO_CONVERSATION_API_BASE_URL, botName = DEFAULT_BOT_NAME, ky = defaultKy diff --git a/src/services/weather.ts b/src/services/weather.ts index dc95a7f4..6060b2ca 100644 --- a/src/services/weather.ts +++ b/src/services/weather.ts @@ -1,5 +1,7 @@ import defaultKy from 'ky' +import { getEnv } from '@/env' + export const WEATHER_API_BASE_URL = 'https://api.weatherapi.com/v1' interface CurrentWeatherResponse { @@ -75,7 +77,7 @@ export class WeatherClient { apiBaseUrl: string constructor({ - apiKey = process.env.WEATHER_API_KEY, + apiKey = getEnv('WEATHER_API_KEY'), apiBaseUrl = WEATHER_API_BASE_URL, ky = defaultKy }: { diff --git a/src/task.ts b/src/task.ts index 02af0032..a00208b8 100644 --- a/src/task.ts +++ b/src/task.ts @@ -18,8 +18,8 @@ import { defaultIDGeneratorFn, isValidTaskIdentifier } from './utils' * * Examples of tasks include: * - LLM calls - * - Chain of LLM calls - * - Retrieval task + * - Chains of LLM calls + * - Retrieval tasks * - API calls * - Native function calls * - Invoking sub-agents @@ -94,6 +94,10 @@ export abstract class BaseTask< return this } + /** + * Ensures that this task is configured correctly. `validate` will be called + * automatically before `task.call` or `task.callWithMetadata` are invoked. + */ public validate() { if (!this._agentic) { throw new Error( @@ -241,6 +245,7 @@ export abstract class BaseTask< (err as any).name === 'TimeoutError' ) { // TODO + return } else if ((err.cause as any)?.code === 'UND_ERR_HEADERS_TIMEOUT') { // TODO: This is a pretty common OpenAI error, and I think it either has // to do with OpenAI's servers being flaky or the combination of Node.js