diff --git a/api/main.py b/api/main.py index 022ab24..c24e81b 100644 --- a/api/main.py +++ b/api/main.py @@ -44,20 +44,19 @@ def semantic(): @app.route('/chat', methods=['POST']) @cross_origin() def chat(): - query = request.json.get('query') session_id = request.json.get('sessionId') history = request.json.get('history', []) - settings = Settings(**request.json.get('settings', {})) - - def run(callback): - return run_query(session_id, query, history, settings, callback) + settings = request.json.get('settings', {}) def formatter(item): if isinstance(item, Exception): item = {'state': 'error', 'error': str(item)} return json.dumps(item) + def run(callback): + return run_query(session_id, query, history, Settings(**settings), callback) + return Response(stream_with_context(stream(stream_callback(run, formatter))), mimetype='text/event-stream') diff --git a/api/src/stampy_chat/chat.py b/api/src/stampy_chat/chat.py index a88c127..1471595 100644 --- a/api/src/stampy_chat/chat.py +++ b/api/src/stampy_chat/chat.py @@ -191,7 +191,12 @@ def run_query(session_id: str, query: str, history: List[Dict], settings: Settin callbacks = [LoggerCallbackHandler(session_id=session_id, query=query, history=history)] if callback: callbacks += [BroadcastCallbackHandler(callback)] - chat_model = get_model(streaming=True, callbacks=callbacks, max_tokens=settings.max_response_tokens) + chat_model = get_model( + streaming=True, + callbacks=callbacks, + max_tokens=settings.max_response_tokens, + model=settings.completions + ) chain = LLMChain( llm=chat_model, diff --git a/web/src/components/html.tsx b/web/src/components/html.tsx index 8312bb7..35e0a00 100644 --- a/web/src/components/html.tsx +++ b/web/src/components/html.tsx @@ -1,8 +1,22 @@ +import { ChangeEvent } from "react"; +import type { Parseable } from "../types"; + // const Colours = ["blue", "cyan", "teal", "green", "amber"].map( // colour => `bg-${colour}-100 border-${colour}-300 text-${colour}-800` // ); // this would be nice, but Tailwind needs te actual string of the class to be in // the source file for it to be included in the build +type NumberParser = (v: Parseable) => number; +type InputFields = { + field: string; + label: string; + value?: Parseable; + min?: string | number; + max?: string | number; + step?: string | number; + parser?: NumberParser; + updater: (v: any) => any; +}; export const Colours = [ "bg-red-100 border-red-300 text-red-800", @@ -30,3 +44,78 @@ export const A: React.FC<{ {children} ); }; + +const between = + ( + min: Parseable, + max: Parseable, + parser: NumberParser, + updater: (v: any) => any + ) => + (event: ChangeEvent) => { + let num = parser((event.target as HTMLInputElement).value); + if (isNaN(num)) { + return; + } else if (min !== undefined && num < parser(min)) { + num = parser(min); + } else if (max !== undefined && num > parser(max)) { + num = parser(max); + } + updater(num); + }; + +export const SectionHeader = ({ text }: { text: string }) => ( +

{text}

+); + +export const NumberInput = ({ + field, + value, + label, + min, + max, + updater, + // this cast is just to satisfy typescript - it can handle numbers, strings and undefined just fine + parser = (v) => parseInt(v as string, 10), +}: InputFields) => ( + <> + + + +); + +export const Slider = ({ + field, + value, + label, + min = 0, + max = 1, + step = 0.01, + // this cast is just to satisfy typescript - it can handle numbers, strings and undefined just fine + parser = (v) => parseFloat(v as string), + updater, +}: InputFields) => ( + <> + + + +); diff --git a/web/src/components/settings.tsx b/web/src/components/settings.tsx new file mode 100644 index 0000000..dfb6b60 --- /dev/null +++ b/web/src/components/settings.tsx @@ -0,0 +1,326 @@ +import { ChangeEvent } from "react"; +import TextareaAutosize from "react-textarea-autosize"; + +import type { Parseable, LLMSettings, Entry, Mode } from "../types"; +import { SectionHeader, NumberInput, Slider } from "../components/html"; + +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: "", + 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", + }, +}; +export const MODELS = { + "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-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; +}; + +/** 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 + ); + +type ChatSettingsParams = { + settings: LLMSettings; + changeSetting: (path: string[], value: any) => void; +}; + +export const ChatSettings = ({ + settings, + changeSetting, +}: ChatSettingsParams) => { + const changeVal = (field: string, value: any) => + changeSetting([field], value); + const update = (field: string) => (event: ChangeEvent) => + changeVal(field, (event.target as HTMLInputElement).value); + const updateNum = (field: string) => (num: Parseable) => + changeVal(field, num); + + return ( +
+ + + + + + + + + + + + + + + + + +
+ ); +}; + +type ChatPromptParams = { + settings: LLMSettings; + query: string; + history: Entry[]; + changeSetting: (path: string[], value: any) => void; +}; + +export const ChatPrompts = ({ + settings, + query, + history, + changeSetting, +}: ChatPromptParams) => { + const updatePrompt = + (...path: string[]) => + (event: ChangeEvent) => + changeSetting( + ["prompts", ...path], + (event.target as HTMLInputElement).value + ); + + return ( +
+
+ Source prompt + +
(This is where sources will be injected)
+
+ {history.length > 0 && ( +
+ History prompt + +
+ History + {history + .slice(Math.max(0, history.length - (settings.maxHistory || 0))) + .map((entry, i) => ( +
+ {entry.content} +
+ ))} +
+
+ )} +
+ Question prompt + + +
+
Q: {query}
+
+ ); +}; diff --git a/web/src/pages/playground.tsx b/web/src/pages/playground.tsx index bb04f1d..8551d87 100644 --- a/web/src/pages/playground.tsx +++ b/web/src/pages/playground.tsx @@ -1,373 +1,29 @@ import type { NextPage } from "next"; -import { useState, useEffect, ChangeEvent } from "react"; -import TextareaAutosize from "react-textarea-autosize"; +import { useRouter } from "next/router"; +import { useState, useEffect } from "react"; import Head from "next/head"; -import Link from "next/link"; import { queryLLM, getStampyContent, runSearch } from "../hooks/useSearch"; import type { Mode, Entry, LLMSettings } from "../types"; import Header from "../components/header"; import Chat from "../components/chat"; import { Controls } from "../components/controls"; - -const MAX_FOLLOWUPS = 4; -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: "", - 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", - }, -}; -const MODELS = { - "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-32k': {maxNumTokens: 32768, topKBlocks: 30}, */ -}; -const DEFAULT_SETTINGS = { - prompts: DEFAULT_PROMPTS, - mode: "default" as Mode, - completions: "gpt-3.5-turbo", - encoder: "cl100k_base", - topKBlocks: MODELS["gpt-3.5-turbo"].topKBlocks, // the number of blocks to use as citations - maxNumTokens: MODELS["gpt-3.5-turbo"].maxNumTokens, - tokensBuffer: 50, // the number of tokens to leave as a buffer when calculating remaining tokens - maxHistory: 10, // the max number of previous items to use as history - historyFraction: 0.25, // the (approximate) fraction of num_tokens to use for history text before truncating - contextFraction: 0.5, // the (approximate) fraction of num_tokens to use for context text before truncating -}; -const ENCODERS = ["cl100k_base"]; - -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 { - updateIn(obj[head], rest, val); - } - return obj; -}; - -type Parseable = string | number | undefined; -type NumberParser = (v: Parseable) => number; -type InputFields = { - field: string; - label: string; - value?: Parseable; - min?: string | number; - max?: string | number; - step?: string | number; - parser?: NumberParser; - updater: (v: any) => any; -}; - -const between = - ( - min: Parseable, - max: Parseable, - parser: NumberParser, - updater: (v: any) => any - ) => - (event: ChangeEvent) => { - let num = parser((event.target as HTMLInputElement).value); - if (isNaN(num)) { - return; - } else if (min !== undefined && num < parser(min)) { - num = parser(min); - } else if (max !== undefined && num > parser(max)) { - num = parser(max); - } - updater(num); - }; - -const SectionHeader = ({ text }: { text: string }) => ( -

{text}

-); - -const NumberInput = ({ - field, - value, - label, - min, - max, - updater, - // this cast is just to satisfy typescript - it can handle numbers, strings and undefined just fine - parser = (v) => parseInt(v as string, 10), -}: InputFields) => ( - <> - - - -); - -const Slider = ({ - field, - value, - label, - min = 0, - max = 1, - step = 0.01, - // this cast is just to satisfy typescript - it can handle numbers, strings and undefined just fine - parser = (v) => parseFloat(v as string), - updater, -}: InputFields) => ( - <> - - - -); - -type ChatSettingsParams = { - settings: LLMSettings; - updateSettings: (updater: (settings: LLMSettings) => LLMSettings) => void; -}; - -const ChatSettings = ({ settings, updateSettings }: ChatSettingsParams) => { - const changeVal = (field: string, value: any) => - updateSettings((prev) => ({ ...prev, [field]: value })); - const update = (setting: string) => (event: ChangeEvent) => { - changeVal(setting, (event.target as HTMLInputElement).value); - }; - const updateNum = (field: string) => (num: Parseable) => - changeVal(field, num); - - return ( -
- - - - - - - - - - - - - - - - - -
- ); -}; - -type ChatPromptParams = { - settings: LLMSettings; - query: string; - history: Entry[]; - updateSettings: (updater: (settings: LLMSettings) => LLMSettings) => void; -}; - -const ChatPrompts = ({ - settings, - query, - history, - updateSettings, -}: ChatPromptParams) => { - const updatePrompt = - (...path: string[]) => - (event: ChangeEvent) => { - const newPrompts = { - ...updateIn( - settings.prompts || {}, - path, - (event.target as HTMLInputElement).value - ), - }; - updateSettings((settings) => ({ ...settings, prompts: newPrompts })); - }; - - return ( -
-
- Source prompt - -
(This is where sources will be injected)
-
- {history.length > 0 && ( -
- History prompt - -
- History - {history - .slice(Math.max(0, history.length - (settings.maxHistory || 0))) - .map((entry, i) => ( -
- {entry.content} -
- ))} -
-
- )} -
- Question prompt - - -
-
Q: {query}
-
- ); -}; +import { + ChatSettings, + ChatPrompts, + updateIn, + makeSettings, +} from "../components/settings"; const Playground: NextPage = () => { const [sessionId, setSessionId] = useState(""); - const [settings, updateSettings] = useState(DEFAULT_SETTINGS); + const [settings, updateSettings] = useState(makeSettings({})); const [query, setQuery] = useState(""); const [history, setHistory] = useState([]); + const router = useRouter(); + const setMode = (mode: [Mode, boolean]) => { if (mode[1]) { localStorage.setItem("chat_mode", mode[0]); @@ -375,6 +31,21 @@ const Playground: NextPage = () => { } }; + const changeSetting = (path: string[], value: any) => { + router.replace( + { + pathname: router.pathname, + query: { + ...router.query, + [path.join(".")]: value.toString(), + }, + }, + undefined, + { scroll: false, shallow: true } + ); + updateSettings((settings) => ({ ...updateIn(settings, path, value) })); + }; + // initial load useEffect(() => { const mode = (localStorage.getItem("chat_mode") as Mode) || "default"; @@ -382,6 +53,10 @@ const Playground: NextPage = () => { setSessionId(crypto.randomUUID()); }, []); + useEffect(() => { + updateSettings(makeSettings(router.query)); + }, [updateSettings, router]); + return ( <> @@ -395,7 +70,7 @@ const Playground: NextPage = () => { settings={settings} query={query} history={history} - updateSettings={updateSettings} + changeSetting={changeSetting} /> { onQuery={setQuery} onNewEntry={setHistory} /> - + diff --git a/web/src/types.ts b/web/src/types.ts index aa99fc1..1b4dc5a 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -64,3 +64,5 @@ export type LLMSettings = { contextFraction?: number; [key: string]: any; }; + +export type Parseable = string | number | undefined;