2023-06-11 02:59:33 +00:00
|
|
|
import { customAlphabet, urlAlphabet } from 'nanoid'
|
2023-06-16 06:49:56 +00:00
|
|
|
import type { ThrottledFunction } from 'p-throttle'
|
2023-06-20 00:23:27 +00:00
|
|
|
import { JsonValue } from 'type-fest'
|
2023-06-11 02:59:33 +00:00
|
|
|
|
|
|
|
import * as types from './types'
|
|
|
|
|
2023-06-15 03:30:16 +00:00
|
|
|
/**
|
|
|
|
* Pauses the execution of a function for a specified time.
|
|
|
|
*
|
|
|
|
* @param ms - number of milliseconds to pause
|
|
|
|
* @returns promise that resolves after the specified number of milliseconds
|
|
|
|
*/
|
2023-06-14 04:39:19 +00:00
|
|
|
export function sleep(ms: number) {
|
|
|
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
|
|
}
|
2023-06-11 02:59:33 +00:00
|
|
|
|
2023-06-15 03:30:16 +00:00
|
|
|
/**
|
|
|
|
* A default ID generator function that uses a custom alphabet based on URL safe symbols.
|
|
|
|
*/
|
2023-06-11 02:59:33 +00:00
|
|
|
export const defaultIDGeneratorFn: types.IDGeneratorFunction =
|
|
|
|
customAlphabet(urlAlphabet)
|
2023-06-14 04:39:19 +00:00
|
|
|
|
2023-06-19 19:59:54 +00:00
|
|
|
const TASK_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if a string is a valid task identifier.
|
|
|
|
*
|
|
|
|
* @param id - identifier to check
|
|
|
|
* @returns whether the identifier is valid
|
|
|
|
*/
|
2023-06-14 04:39:19 +00:00
|
|
|
export function isValidTaskIdentifier(id: string): boolean {
|
2023-06-19 19:59:54 +00:00
|
|
|
return !!id && TASK_NAME_REGEX.test(id)
|
2023-06-14 04:39:19 +00:00
|
|
|
}
|
2023-06-15 03:00:02 +00:00
|
|
|
|
2023-06-19 19:59:54 +00:00
|
|
|
/**
|
|
|
|
* Extracts a valid function task identifier from the input text string.
|
|
|
|
*
|
|
|
|
* @param text - input text string to extract the identifier from
|
|
|
|
* @returns extracted task identifier if one is found, `undefined` otherwise
|
|
|
|
*/
|
2023-06-16 06:49:56 +00:00
|
|
|
export function extractFunctionIdentifierFromString(
|
|
|
|
text: string
|
|
|
|
): string | undefined {
|
|
|
|
text = text?.trim()
|
|
|
|
|
|
|
|
if (!text) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isValidTaskIdentifier(text)) {
|
|
|
|
return text
|
|
|
|
}
|
|
|
|
|
|
|
|
const splits = text
|
|
|
|
.split(/[^a-zA-Z0-9_-]/)
|
|
|
|
.map((s) => {
|
|
|
|
s = s.trim()
|
|
|
|
return isValidTaskIdentifier(s) ? s : undefined
|
|
|
|
})
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
|
|
return splits[splits.length - 1]
|
|
|
|
}
|
|
|
|
|
2023-06-15 03:00:02 +00:00
|
|
|
/**
|
2023-06-15 03:30:16 +00:00
|
|
|
* Chunks a string into an array of chunks.
|
2023-06-15 03:00:02 +00:00
|
|
|
*
|
|
|
|
* @param text - string to chunk
|
2023-06-15 03:30:16 +00:00
|
|
|
* @param maxLength - maximum length of each chunk
|
|
|
|
* @returns array of chunks
|
2023-06-15 03:00:02 +00:00
|
|
|
*/
|
2023-06-16 00:58:37 +00:00
|
|
|
export function chunkString(text: string, maxLength: number): string[] {
|
2023-06-15 03:00:02 +00:00
|
|
|
const words = text.split(' ')
|
|
|
|
const chunks: string[] = []
|
|
|
|
let chunk = ''
|
|
|
|
|
|
|
|
for (const word of words) {
|
2023-06-15 03:30:16 +00:00
|
|
|
if (word.length > maxLength) {
|
2023-06-15 03:00:02 +00:00
|
|
|
// Truncate the word if it's too long and indicate that it was truncated:
|
2023-06-15 03:30:16 +00:00
|
|
|
chunks.push(word.substring(0, maxLength - 3) + '...')
|
2023-06-17 00:03:02 +00:00
|
|
|
} else if ((chunk + ' ' + word).length > maxLength) {
|
2023-06-15 03:00:02 +00:00
|
|
|
chunks.push(chunk.trim())
|
|
|
|
chunk = word
|
|
|
|
} else {
|
2023-06-15 03:30:16 +00:00
|
|
|
chunk += (chunk ? ' ' : '') + word
|
2023-06-15 03:00:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (chunk) {
|
|
|
|
chunks.push(chunk.trim())
|
|
|
|
}
|
|
|
|
|
|
|
|
return chunks
|
|
|
|
}
|
2023-06-16 00:58:37 +00:00
|
|
|
|
2023-06-17 00:03:02 +00:00
|
|
|
/**
|
|
|
|
* Chunks an array of strings into an array of chunks while preserving existing sections.
|
|
|
|
*
|
|
|
|
* @param textSections - array of strings to chunk
|
|
|
|
* @param maxLength - maximum length of each chunk
|
|
|
|
* @returns array of chunks
|
|
|
|
*/
|
|
|
|
export function chunkMultipleStrings(
|
|
|
|
textSections: string[],
|
|
|
|
maxLength: number
|
|
|
|
): string[] {
|
|
|
|
return textSections.map((section) => chunkString(section, maxLength)).flat()
|
|
|
|
}
|
|
|
|
|
2023-06-16 00:58:37 +00:00
|
|
|
/**
|
|
|
|
* Stringifies a JSON value for use in an LLM prompt.
|
|
|
|
*
|
|
|
|
* @param json - JSON value to stringify
|
|
|
|
* @returns stringified value with all double quotes around object keys removed
|
|
|
|
*/
|
2023-06-20 00:23:27 +00:00
|
|
|
export function stringifyForModel(
|
|
|
|
json: types.Jsonifiable,
|
|
|
|
omit: string[] = []
|
|
|
|
): string {
|
2023-06-16 00:58:37 +00:00
|
|
|
const UNIQUE_PREFIX = defaultIDGeneratorFn()
|
|
|
|
return (
|
|
|
|
JSON.stringify(json, replacer)
|
|
|
|
// Remove all double quotes around keys:
|
|
|
|
.replace(new RegExp('"' + UNIQUE_PREFIX + '(.*?)"', 'g'), '$1')
|
|
|
|
)
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Replacer function prefixing all keys with a unique identifier.
|
|
|
|
*/
|
2023-06-20 00:23:27 +00:00
|
|
|
function replacer(key: string, value: JsonValue) {
|
|
|
|
if (omit.includes(key)) {
|
|
|
|
return undefined
|
|
|
|
}
|
|
|
|
|
2023-06-16 00:58:37 +00:00
|
|
|
if (value && typeof value === 'object') {
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
return value
|
|
|
|
}
|
|
|
|
|
|
|
|
const replacement = {}
|
|
|
|
|
|
|
|
for (const k in value) {
|
2023-06-20 00:23:27 +00:00
|
|
|
if (Object.hasOwnProperty.call(value, k) && !omit.includes(k)) {
|
2023-06-16 00:58:37 +00:00
|
|
|
replacement[UNIQUE_PREFIX + k] = value[k]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return replacement
|
|
|
|
}
|
|
|
|
|
|
|
|
return value
|
|
|
|
}
|
|
|
|
}
|
2023-06-16 06:49:56 +00:00
|
|
|
|
2023-06-16 15:22:21 +00:00
|
|
|
/**
|
|
|
|
* Picks keys from an object.
|
|
|
|
*
|
|
|
|
* @param obj - object to pick keys from
|
|
|
|
* @param keys - keys to pick from the object
|
|
|
|
* @returns new object with only the picked keys
|
|
|
|
*/
|
|
|
|
export function pick<T extends object, K extends keyof T>(
|
|
|
|
obj: T,
|
|
|
|
...keys: K[]
|
|
|
|
) {
|
|
|
|
return keys.reduce((result, key) => {
|
|
|
|
result[key] = obj[key]
|
|
|
|
return result
|
|
|
|
}, {} as Pick<T, K>)
|
2023-06-16 06:49:56 +00:00
|
|
|
}
|
|
|
|
|
2023-06-16 15:22:21 +00:00
|
|
|
/**
|
|
|
|
* Omits keys from an object.
|
|
|
|
*
|
|
|
|
* @param obj - object to omit keys from
|
|
|
|
* @param keys - keys to omit from the object
|
|
|
|
* @returns new object without the omitted keys
|
|
|
|
*/
|
|
|
|
export function omit<T extends object, K extends keyof T>(
|
|
|
|
obj: T,
|
|
|
|
...keys: K[]
|
|
|
|
) {
|
|
|
|
const keySet = new Set(keys)
|
|
|
|
return Object.keys(obj).reduce((result, key) => {
|
|
|
|
if (!keySet.has(key as K)) {
|
|
|
|
result[key] = obj[key as keyof T]
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}, {} as Omit<T, K>)
|
2023-06-16 06:49:56 +00:00
|
|
|
}
|
|
|
|
|
2023-06-16 15:22:21 +00:00
|
|
|
/**
|
|
|
|
* Function that does nothing.
|
|
|
|
*/
|
2023-06-16 06:49:56 +00:00
|
|
|
const noop = () => undefined
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Throttles HTTP requests made by a ky instance. Very useful for enforcing rate limits.
|
|
|
|
*/
|
|
|
|
export function throttleKy(
|
|
|
|
ky: types.KyInstance,
|
|
|
|
throttleFn: <Argument extends readonly unknown[], ReturnValue>(
|
|
|
|
function_: (...args: Argument) => ReturnValue
|
|
|
|
) => ThrottledFunction<Argument, ReturnValue>
|
|
|
|
) {
|
|
|
|
return ky.extend({
|
|
|
|
hooks: {
|
|
|
|
beforeRequest: [throttleFn(noop)]
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|