chatgpt-api/packages/core/src/utils.ts

298 wiersze
7.2 KiB
TypeScript

import dedent from 'dedent'
import hashObjectImpl, { type Options as HashObjectOptions } from 'hash-object'
import type * as types from './types'
export { assert } from './assert'
export { default as delay } from 'delay'
/**
* From `inputObj`, create a new object that does not include `keys`.
*
* @example
* ```js
* omit({ a: 1, b: 2, c: 3 }, 'a', 'c') // { b: 2 }
* ```
*/
export const omit = <
T extends Record<string, unknown> | object,
K extends keyof any
>(
inputObj: T,
...keys: K[]
): Omit<T, K> => {
const keysSet = new Set(keys)
return Object.fromEntries(
Object.entries(inputObj).filter(([k]) => !keysSet.has(k as any))
) as any
}
/**
* From `inputObj`, create a new object that only includes `keys`.
*
* @example
* ```js
* pick({ a: 1, b: 2, c: 3 }, 'a', 'c') // { a: 1, c: 3 }
* ```
*/
export const pick = <
T extends Record<string, unknown> | object,
K extends keyof T
>(
inputObj: T,
...keys: K[]
): Pick<T, K> => {
const keysSet = new Set(keys)
return Object.fromEntries(
Object.entries(inputObj).filter(([k]) => keysSet.has(k as any))
) as any
}
export function pruneUndefined<T extends Record<string, any>>(
obj: T
): NonNullable<{ [K in keyof T]: Exclude<T[K], undefined> }> {
return Object.fromEntries(
Object.entries(obj).filter(([, value]) => value !== undefined)
) as NonNullable<T>
}
export function pruneNullOrUndefined<T extends Record<string, any>>(
obj: T
): NonNullable<{ [K in keyof T]: Exclude<T[K], undefined | null> }> {
return Object.fromEntries(
Object.entries(obj).filter(
([, value]) => value !== undefined && value !== null
)
) as NonNullable<T>
}
export function pruneNullOrUndefinedDeep<T extends Record<string, any>>(
obj: T
): NonNullable<{ [K in keyof T]: Exclude<T[K], undefined | null> }> {
if (!obj || Array.isArray(obj) || typeof obj !== 'object') return obj
return Object.fromEntries(
Object.entries(obj)
.filter(([, value]) => value !== undefined && value !== null)
.map(([key, value]) =>
Array.isArray(value)
? [
key,
value
.filter((v) => v !== undefined && v !== null)
.map(pruneNullOrUndefinedDeep as any)
]
: typeof value === 'object'
? [key, pruneNullOrUndefinedDeep(value)]
: [key, value]
)
) as NonNullable<T>
}
export function pruneEmpty<T extends Record<string, any>>(
obj: T
): NonNullable<{ [K in keyof T]: Exclude<T[K], undefined | null> }> {
return Object.fromEntries(
Object.entries(obj).filter(([, value]) => {
if (value === undefined || value === null) return false
if (typeof value === 'string' && !value) return false
if (Array.isArray(value) && !value.length) return false
if (
typeof value === 'object' &&
!Array.isArray(value) &&
!Object.keys(value).length
) {
return false
}
return true
})
) as NonNullable<T>
}
export function pruneEmptyDeep<T>(
value?: T
):
| undefined
| (T extends Record<string, any>
? { [K in keyof T]: Exclude<T[K], undefined | null> }
: T extends Array<infer U>
? Array<Exclude<U, undefined | null>>
: Exclude<T, null>) {
if (value === undefined || value === null) return undefined
if (typeof value === 'string') {
if (!value) return undefined
return value as any
}
if (Array.isArray(value)) {
if (!value.length) return undefined
value = value
.map((v) => pruneEmptyDeep(v))
.filter((v) => v !== undefined) as any
if (!value || !Array.isArray(value) || !value.length) return undefined
return value as any
}
if (typeof value === 'object') {
if (!Object.keys(value).length) return undefined
value = Object.fromEntries(
Object.entries(value)
.map(([k, v]) => [k, pruneEmptyDeep(v)])
.filter(([, v]) => v !== undefined)
)
if (!value || !Object.keys(value).length) return undefined
return value as any
}
return value as any
}
export function getEnv(name: string): string | undefined {
try {
return typeof process !== 'undefined'
? // eslint-disable-next-line no-process-env
process.env?.[name]
: undefined
} catch {
return undefined
}
}
/**
* Function that does nothing.
*/
export const noop = () => undefined
/**
* Throttles HTTP requests made by a ky instance.
*
* Very useful for enforcing rate limits.
*/
export function throttleKy(
ky: types.KyInstance,
throttleFn: <Arguments extends readonly unknown[], ReturnValue>(
function_: (...args_: Arguments) => ReturnValue
) => types.ThrottledFunction<(...args_: Arguments) => ReturnValue>
) {
return ky.extend({
hooks: {
beforeRequest: [throttleFn(noop)]
}
})
}
/**
* Creates a new `URLSearchParams` object with all values coerced to strings
* that correctly handles arrays of values as repeated keys.
*/
export function sanitizeSearchParams(
searchParams:
| Record<
string,
string | number | boolean | string[] | number[] | boolean[] | undefined
>
| object,
{ csv = false }: { csv?: boolean } = {}
): URLSearchParams {
const entries = Object.entries(searchParams).flatMap(([key, value]) => {
if (key === undefined || value === undefined) {
return []
}
if (Array.isArray(value)) {
return value.map((v) => [key, String(v)])
}
return [[key, String(value)]]
}) as [string, string][]
if (!csv) {
return new URLSearchParams(entries)
}
const csvEntries = entries.reduce(
(acc, [key, value]) => ({
...acc,
[key]: acc[key] ? `${acc[key]},${value}` : value
}),
{} as any
)
return new URLSearchParams(csvEntries)
}
/**
* Stringifies a JSON value in a way that's optimized for use with LLM prompts.
*/
export function stringifyForModel(
jsonObject?: types.RelaxedJsonifiable
): string {
if (jsonObject === undefined) {
return ''
}
if (typeof jsonObject === 'string') {
return jsonObject
}
return JSON.stringify(jsonObject, null, 0)
}
const dedenter = dedent.withOptions({ escapeSpecialCharacters: true })
/**
* Clean a string by removing extra newlines and indentation.
*
* @see: https://github.com/dmnd/dedent
*/
export function cleanStringForModel(text: string): string {
return dedenter(text).trim()
}
export function hashObject(
object: Record<string, any>,
options?: HashObjectOptions
): string {
return hashObjectImpl(object, { algorithm: 'sha256', ...options })
}
export function isAIFunction(obj: any): obj is types.AIFunction {
if (!obj) return false
if (typeof obj !== 'function') return false
if (!obj.inputSchema) return false
if (!obj.parseInput) return false
if (!obj.spec) return false
if (!obj.impl) return false
if (!obj.spec.name || typeof obj.spec.name !== 'string') return false
return true
}
export function getErrorMessage(error?: unknown): string {
if (!error) {
return 'unknown error'
}
if (typeof error === 'string') {
return error
}
const message = (error as any).message
if (message && typeof message === 'string') {
return message
}
try {
return JSON.stringify(error)
} catch {
return 'unknown error'
}
}