Files
stampy-chat/web/src/hooks/useSettings.ts
T

234 lines
8.2 KiB
TypeScript

import { useRouter } from "next/router";
import { useState, useEffect, useCallback } from "react";
import type { CurrentSearch, Mode, Entry, LLMSettings } from "../types";
type LLMSettingsParsers = {
[key: string]:
| ((v: number | undefined) => any)
| ((v: string | undefined) => any)
| ((v: object | undefined) => any);
};
const DEFAULT_PROMPTS = {
context:
"You are a helpful assistant knowledgeable about AI Alignment and Safety. " +
'Please give a clear and coherent answer to the user\'s questions.(written after "Q:") ' +
"using the following sources. Each source is labeled with a letter. Feel free to " +
"use the sources in any order, and try to use multiple sources in your answers.\n\n",
history:
"\n\n" +
'Before the question ("Q: "), there will be a history of previous questions and answers. ' +
"These sources only apply to the last question. any sources used in previous answers " +
"are invalid.",
question:
"In your answer, please cite any claims you make back to each source " +
"using the format: [a], [b], etc. If you use multiple sources to make a claim " +
'cite all of them. For example: "AGI is concerning [c, d, e]."\n\n',
modes: {
default: "",
discord:
"Your answer will be used in a Discord channel, so please Answer concisely, getting to " +
"the crux of the matter in as few words as possible. Limit your answer to 1-2 paragraphs.\n\n",
concise:
"Answer very concisely, getting to the crux of the matter in as " +
"few words as possible. Limit your answer to 1-2 sentences.\n\n",
rookie:
"This user is new to the field of AI Alignment and Safety - don't " +
"assume they know any technical terms or jargon. Still give a complete answer " +
"without patronizing the user, but take any extra time needed to " +
"explain new concepts or to illustrate your answer with examples. " +
"Put extra effort into explaining the intuition behind concepts " +
"rather than just giving a formal definition.\n\n",
},
};
interface Model {
maxNumTokens: number;
topKBlocks: number;
}
export const MODELS: { [key: string]: Model } = {
"gpt-3.5-turbo": { maxNumTokens: 4095, topKBlocks: 10 },
"gpt-3.5-turbo-16k": { maxNumTokens: 16385, topKBlocks: 30 },
"gpt-4": { maxNumTokens: 8192, topKBlocks: 20 },
"gpt-4-1106-preview": { maxNumTokens: 128000, topKBlocks: 50 },
/* 'gpt-4-32k': {maxNumTokens: 32768, topKBlocks: 30}, */
};
export const ENCODERS = ["cl100k_base"];
/** Update the given `obj` so that it has `val` at the given path.
*
* e.g.
* updateIn({a: {b: 123}}, ['a', 'b', 'c'], 42) == {a: {b: 123, c: 42}}
* updateIn({a: {b: 123}}, ['z', 'y', 'x'], 42) == {a: {b: 123}, z: {y: {x: 42}}}
*/
export const updateIn = (
obj: { [key: string]: any },
[head, ...rest]: string[],
val: any
) => {
if (!head) {
// No path provided - do nothing
} else if (!rest || rest.length == 0) {
obj[head] = val;
} else {
if (obj[head] === undefined) {
obj[head] = {};
}
updateIn(obj[head], rest, val);
}
return obj;
};
const randomElement = (array: any[]) =>
array[Math.floor(Math.random() * array.length)];
const randomFloat = (min: number, max: number) =>
Math.random() * (max - min) + min;
const randomInt = (min: number, max: number) =>
Math.floor(randomFloat(min, max));
/** Create a settings object in which all items in the `overrides` object will be parsed appropriately
*
* `parsers` should be an object mapping settings fields to functions that will return a valid setting.
* The parser functions should have default values that will be used if the provided value is undefined.
*/
const parseSettings = (overrides: LLMSettings, parsers: LLMSettingsParsers) =>
Object.entries(parsers).reduce(
(settings, [key, parser]) =>
updateIn(settings, [key], parser(overrides[key])),
{}
);
/** Make a parser function from the provided `defaultVal`.
*
* If the parsed value is undefined, `defaultVal` will be returned, otherwise it will be parsed as
* a value of the same type as `defaultVal`.
* If `defaultVal` is an object, it will return a parser that will recursively search for appropriate keys.
*/
const withDefault = (defaultVal: any) => {
if (typeof defaultVal === "number" && defaultVal % 1 === 0) {
return (v: string | undefined): number =>
v !== undefined ? parseInt(v, 10) : defaultVal;
} else if (typeof defaultVal === "number") {
return (v: string | undefined): number =>
v !== undefined ? parseFloat(v) : defaultVal;
} else if (typeof defaultVal === "object") {
const parsers = Object.entries(defaultVal).reduce(
(parsers, [key, val]) => updateIn(parsers, [key], withDefault(val)),
{}
);
return (v: object | undefined): object => parseSettings(v || {}, parsers);
} else {
return (v: any | undefined): any => v || defaultVal;
}
};
const SETTINGS_PARSERS = {
prompts: withDefault(DEFAULT_PROMPTS),
mode: (v: string | undefined) => (v || "default") as Mode,
completions: withDefault("gpt-3.5-turbo"),
encoder: withDefault("cl100k_base"),
topKBlocks: withDefault(MODELS["gpt-3.5-turbo"]?.topKBlocks), // the number of blocks to use as citations
maxNumTokens: withDefault(MODELS["gpt-3.5-turbo"]?.maxNumTokens),
tokensBuffer: withDefault(50), // the number of tokens to leave as a buffer when calculating remaining tokens
maxHistory: withDefault(10), // the max number of previous items to use as history
historyFraction: withDefault(0.25), // the (approximate) fraction of num_tokens to use for history text before truncating
contextFraction: withDefault(0.5), // the (approximate) fraction of num_tokens to use for context text before truncating
};
export const makeSettings = (overrides: LLMSettings) =>
parseSettings(
Object.entries(overrides).reduce(
(acc, [key, val]) => updateIn(acc, key.split("."), val),
{}
),
SETTINGS_PARSERS
);
const randomSettings = () => {
const completions = randomElement(Object.keys(MODELS));
const model = MODELS[completions] as Model;
const maxNumTokens = randomInt(
Math.floor(model.maxNumTokens * 0.3),
model.maxNumTokens
);
const historyFraction = randomFloat(0.2, 0.8);
const contextFraction = randomFloat(0.2, 0.9 - historyFraction);
return makeSettings({
completions,
maxNumTokens,
historyFraction,
contextFraction,
mode: randomElement(Object.keys(DEFAULT_PROMPTS.modes)) as Mode,
topKBlocks: randomInt(Math.floor(model.topKBlocks * 0.3), model.topKBlocks),
tokensBuffer: randomInt(10, 200),
maxHistory: randomInt(1, 20),
});
};
type ChatSettingsParams = {
settings: LLMSettings;
changeSetting: (path: string[], value: any) => void;
};
type SettingsUpdatePair = [path: string[], val: any];
export default function useSettings() {
const [settingsLoaded, setLoaded] = useState(false);
const [settings, updateSettings] = useState<LLMSettings>(makeSettings({}));
const router = useRouter();
const updateInUrl = (vals: { [key: string]: any }) =>
router.replace({
pathname: router.pathname,
query: { ...router.query, ...vals },
});
const changeSetting = (path: string[], value: any) => {
updateInUrl({ [path.join(".")]: value });
updateSettings((settings) => ({ ...updateIn(settings, path, value) }));
};
const changeSettings = (...items: SettingsUpdatePair) => {
updateInUrl(
items.reduce(
(acc, [path, val]) => ({ ...acc, [path.join(".")]: val }),
{}
)
);
updateSettings((settings) =>
items.reduce(
(acc, [path, val]) => ({ ...acc, ...updateIn(settings, path, val) }),
settings
)
);
};
const setMode = (mode: Mode | undefined) => {
if (mode) {
updateSettings({ ...settings, mode: mode });
localStorage.setItem("chat_mode", mode);
}
};
useEffect(() => {
if (!router.isReady) return;
const mode = (router?.query?.mode ||
localStorage.getItem("chat_mode") ||
"default") as Mode;
updateSettings(makeSettings({ ...router.query, mode }));
setLoaded(router.isReady);
}, [router]);
const randomize = useCallback(() => updateSettings(randomSettings()), []);
return {
settings,
changeSetting,
changeSettings,
setMode,
settingsLoaded,
randomize,
};
}