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..5d7f8f4 100644 --- a/api/src/stampy_chat/chat.py +++ b/api/src/stampy_chat/chat.py @@ -1,6 +1,6 @@ from typing import Any, Callable, Dict, List -from langchain.chains import LLMChain, OpenAIModerationChain, moderation +from langchain.chains import LLMChain, OpenAIModerationChain from langchain.chat_models import ChatOpenAI from langchain.memory import ChatMessageHistory, ConversationSummaryBufferMemory from langchain.prompts import ( @@ -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/chat.tsx b/web/src/components/chat.tsx index e6e8498..e6fd490 100644 --- a/web/src/components/chat.tsx +++ b/web/src/components/chat.tsx @@ -9,6 +9,7 @@ import type { LLMSettings, Followup, } from "../types"; +import useCitations from "../hooks/useCitations"; import { SearchBox } from "../components/searchbox"; import { AssistantEntry } from "../components/assistant"; import { Entry as EntryTag } from "../components/entry"; @@ -40,6 +41,38 @@ function scroll30() { window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }); } +export const ChatResponse = ({ + current, + defaultElem, +}: { + current: CurrentSearch; + defaultElem?: any; +}) => { + switch (current?.phase) { + case "started": + return

Loading: Sending query...

; + case "semantic": + return

Loading: Performing semantic search...

; + case "context": + return

Loading: Creating context...

; + case "prompt": + return

Loading: Creating prompt...

; + case "llm": + return

Loading: Waiting for LLM...

; + case "streaming": + return ; + case "followups": + return ( + <> + +

Checking for followups...

+ + ); + default: + return defaultElem; + } +}; + type ChatParams = { sessionId: string; settings: LLMSettings; @@ -50,46 +83,17 @@ type ChatParams = { const Chat = ({ sessionId, settings, onQuery, onNewEntry }: ChatParams) => { const [entries, setEntries] = useState([]); const [current, setCurrent] = useState(); - const [citations, setCitations] = useState([]); + const { citations, setEntryCitations } = useCitations(); const updateCurrent = (current: CurrentSearch) => { - setCurrent(current); if (current?.phase === "streaming") { + setCurrent(setEntryCitations(current)); scroll30(); + } else { + setCurrent(current); } }; - const updateCitations = ( - allCitations: Citation[], - current?: CurrentSearch - ) => { - if (!current) return; - - const entryCitations = Array.from(current.citationsMap.values()); - if (!entryCitations.some((c) => !c.index)) { - // All of the entries citations have indexes, so there weren't any changes since the last check - return; - } - - // Get a mapping of all known citations, so as to reuse them if they appear again - const citationsMapping = Object.fromEntries( - allCitations.map((c) => [c.title + c.url, c.index]) - ); - - entryCitations.forEach((c) => { - const hash = c.title + c.url; - const index = citationsMapping[hash]; - if (!index) { - c.index = allCitations.length + 1; - allCitations.push(c); - } else { - c.index = index; - } - }); - setCitations(allCitations); - setCurrent(current); - }; - const addEntry = (entry: Entry) => { setEntries((prev) => { const entries = [...prev, entry]; @@ -132,36 +136,6 @@ const Chat = ({ sessionId, settings, onQuery, onNewEntry }: ChatParams) => { setCurrent(undefined); }; - var last_entry = <>; - switch (current?.phase) { - case "semantic": - last_entry =

Loading: Performing semantic search...

; - break; - case "prompt": - last_entry =

Loading: Creating prompt...

; - break; - case "llm": - last_entry =

Loading: Waiting for LLM...

; - break; - case "streaming": - updateCitations(citations, current); - last_entry = ; - break; - case "followups": - last_entry = ( - <> - -

Checking for followups...

- - ); - break; - default: - last_entry = ( - - ); - break; - } - return (
    {entries.map( @@ -185,8 +159,12 @@ const Chat = ({ sessionId, settings, onQuery, onNewEntry }: ChatParams) => { ) )} - - {last_entry} + setEntries([])}>Clear history + } + />
); }; diff --git a/web/src/components/controls.tsx b/web/src/components/controls.tsx index a92ae63..99524e6 100644 --- a/web/src/components/controls.tsx +++ b/web/src/components/controls.tsx @@ -1,57 +1,39 @@ import type { Mode } from "../types"; -export const Controls = ({ - mode, - setMode, -}: { - mode: [Mode, boolean]; +const MODES = { + rookie: + "For people who are new to the field of AI alignment. The " + + "answer might be longer, since technical terms will be " + + "explained in more detail and less background will be assumed.", + concise: + "Quick and to the point. Followup questions may need to be " + + "asked to get the full picture of what's going on.", + default: "A balanced default mode.", +}; + +type ControlsType = { + mode: Mode; setMode: (m: any) => void; -}) => { +}; +export const Controls = ({ mode, setMode }: ControlsType) => { { /* three buttons for the three modes, place far right, 1rem between each */ } return (
- - - + {Object.entries(MODES).map(([modeType, title]) => ( + + ))}
); }; diff --git a/web/src/components/header.tsx b/web/src/components/header.tsx index 1941361..641a53c 100644 --- a/web/src/components/header.tsx +++ b/web/src/components/header.tsx @@ -3,9 +3,9 @@ import Link from "next/link"; import Image from "next/image"; import logo from "../logo.svg"; -const Header: React.FC<{ page: "index" | "semantic" | "playground" }> = ({ - page, -}) => { +export type Page = "index" | "semantic" | "playground" | "tester"; + +const Header: React.FC<{ page: Page }> = ({ page }) => { const sidebar = page === "index" ? ( 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/page.tsx b/web/src/components/page.tsx index f64337b..8f3a486 100644 --- a/web/src/components/page.tsx +++ b/web/src/components/page.tsx @@ -1,17 +1,19 @@ import React, { ReactNode } from "react"; import Head from "next/head"; +import type { Page as PageType } from "./header"; import Header from "./header"; -const Page: React.FC<{ children: ReactNode; page: "index" | "semantic" }> = ({ - page, - children, -}) => { +const Page: React.FC<{ + children: ReactNode; + widescreen?: boolean; + page: PageType; +}> = ({ page, children, widescreen = false }) => { return ( <> AI Safety Info -
+
{children}
diff --git a/web/src/components/searchbox.tsx b/web/src/components/searchbox.tsx index 1e91c47..0b7bd61 100644 --- a/web/src/components/searchbox.tsx +++ b/web/src/components/searchbox.tsx @@ -1,33 +1,10 @@ import React from "react"; import { useState, useEffect } from "react"; +import { initialQuestions } from "../settings"; import type { Followup } from "../types"; import TextareaAutosize from "react-textarea-autosize"; import dynamic from "next/dynamic"; -// initial questions to fill the search box with. -export const initialQuestions: string[] = [ - "Are there any regulatory efforts aimed at addressing AI safety and alignment concerns?", - "How can I help with AI safety and alignment?", - "How could a predictive model - like an LLM - act like an agent?", - "How could an AI possibly be an x-risk when some populations aren't even connected to the internet?", - "I'm not convinced, why is this important?", - "Summarize the differences in opinion between Eliezer Yudkowsky and Paul Christiano.", - 'What are "RAAPs"?', - 'What are "scaling laws" and how are they relevant to safety?', - "What are some of the different research approaches?", - "What are the differences between Inner and Outer alignment?", - 'What does the term "x-risk" mean?', - 'What is "FOOM"?', - 'What is "instrumental convergence"?', - "What is a hard takeoff?", - "What is a mesa-optimizer?", - "What is AI safety and alignment?", - "What is an AI arms race?", - "What is an Intelligence Explosion?", - 'What is the "orthogonality thesis"?', - 'Why would we expect AI to be "misaligned by default"?', -]; - const SearchBoxInternal: React.FC<{ search: ( query: string, diff --git a/web/src/components/settings.tsx b/web/src/components/settings.tsx new file mode 100644 index 0000000..2b3ffc5 --- /dev/null +++ b/web/src/components/settings.tsx @@ -0,0 +1,201 @@ +import { ChangeEvent } from "react"; +import TextareaAutosize from "react-textarea-autosize"; + +import type { Parseable, LLMSettings, Entry, Mode } from "../types"; +import { MODELS, ENCODERS } from "../hooks/useSettings"; +import { SectionHeader, NumberInput, Slider } from "../components/html"; + +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/hooks/useCitations.ts b/web/src/hooks/useCitations.ts new file mode 100644 index 0000000..4017859 --- /dev/null +++ b/web/src/hooks/useCitations.ts @@ -0,0 +1,47 @@ +import { useState } from "react"; + +import type { CurrentSearch, Citation } from "../types"; + +const updateCitations = ( + allCitations: Citation[], + setCitations: (citations: Citation[]) => any, + entry?: CurrentSearch +) => { + if (!entry) return entry; + + const entryCitations = Array.from(entry.citationsMap.values()); + if (!entryCitations.some((c) => !c.index)) { + // All of the entries citations have indexes, so there weren't any changes since the last check + return entry; + } + + // Get a mapping of all known citations, so as to reuse them if they appear again + const citationsMapping = Object.fromEntries( + allCitations.map((c) => [c.title + c.url, c.index]) + ); + + entryCitations.forEach((c) => { + const hash = c.title + c.url; + const index = citationsMapping[hash]; + if (!index) { + c.index = allCitations.length + 1; + allCitations.push(c); + } else { + c.index = index; + } + }); + setCitations(allCitations); + return entry; +}; + +export default function useCitations() { + const [citations, setCitations] = useState([]); + + const setEntryCitations = (entry: CurrentSearch) => + updateCitations(citations, setCitations, entry); + + return { + citations, + setEntryCitations, + }; +} diff --git a/web/src/hooks/useSearch.ts b/web/src/hooks/useSearch.ts index de0a62a..9c7b166 100644 --- a/web/src/hooks/useSearch.ts +++ b/web/src/hooks/useSearch.ts @@ -57,16 +57,19 @@ export async function* iterateData(res: Response) { } } -export const extractAnswer = async ( - res: Response, - setCurrent: (e: CurrentSearch) => void -): Promise => { - var result: AssistantEntry = { +const makeEntry = () => + ({ role: "assistant", content: "", citations: [], citationsMap: new Map(), - }; + } as AssistantEntry); + +export const extractAnswer = async ( + res: Response, + setCurrent: (e: CurrentSearch) => void +): Promise => { + var result: AssistantEntry = makeEntry(); var followups: Followup[] = []; for await (var data of iterateData(res)) { switch (data.state) { @@ -135,6 +138,7 @@ export const queryLLM = async ( sessionId: string, controller: AbortController ): Promise => { + setCurrent({ ...makeEntry(), phase: "started" }); // do SSE on a POST request. const res = await fetchLLM(sessionId, query, settings, history, controller); diff --git a/web/src/hooks/useSettings.ts b/web/src/hooks/useSettings.ts new file mode 100644 index 0000000..e96b159 --- /dev/null +++ b/web/src/hooks/useSettings.ts @@ -0,0 +1,172 @@ +import { useRouter } from "next/router"; +import { useState, useEffect } 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: "", + 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 default function useSettings() { + const [settings, updateSettings] = useState(makeSettings({})); + const router = useRouter(); + + const updateInUrl = (path: string[], value: any) => + router.replace({ + pathname: router.pathname, + query: { + ...router.query, + [path.join(".")]: value.toString(), + }, + }); + + const changeSetting = (path: string[], value: any) => { + updateInUrl(path, value); + updateSettings((settings) => ({ ...updateIn(settings, path, value) })); + }; + + const setMode = (mode: Mode | undefined) => { + if (mode) { + updateSettings({ ...settings, mode: mode }); + localStorage.setItem("chat_mode", mode); + } + }; + + useEffect(() => { + const mode = (localStorage.getItem("chat_mode") as Mode) || "default"; + updateSettings(makeSettings({ ...router.query, mode: mode })); + }, [updateSettings, router]); + + return { + settings, + changeSetting, + setMode, + }; +} diff --git a/web/src/pages/index.tsx b/web/src/pages/index.tsx index 41482d1..d1f9071 100644 --- a/web/src/pages/index.tsx +++ b/web/src/pages/index.tsx @@ -3,6 +3,7 @@ import { useState, useEffect } from "react"; import Link from "next/link"; import { queryLLM, getStampyContent, runSearch } from "../hooks/useSearch"; +import useSettings from "../hooks/useSettings"; import type { Mode } from "../types"; import Page from "../components/page"; import Chat from "../components/chat"; @@ -12,23 +13,16 @@ const MAX_FOLLOWUPS = 4; const Home: NextPage = () => { const [sessionId, setSessionId] = useState(""); - const [mode, setMode] = useState<[Mode, boolean]>(["default", false]); - - // store mode in localstorage - useEffect(() => { - if (mode[1]) localStorage.setItem("chat_mode", mode[0]); - }, [mode]); + const { settings, setMode } = useSettings(); // initial load useEffect(() => { - const mode = (localStorage.getItem("chat_mode") as Mode) || "default"; - setMode([mode, true]); setSessionId(crypto.randomUUID()); }, []); return ( - +

WARNING: This is a very early prototype.{" "} @@ -38,7 +32,7 @@ const Home: NextPage = () => { welcomed.

- +
); }; diff --git a/web/src/pages/playground.tsx b/web/src/pages/playground.tsx index bb04f1d..a90ba0e 100644 --- a/web/src/pages/playground.tsx +++ b/web/src/pages/playground.tsx @@ -1,412 +1,44 @@ import type { NextPage } from "next"; -import { useState, useEffect, ChangeEvent } from "react"; -import TextareaAutosize from "react-textarea-autosize"; -import Head from "next/head"; -import Link from "next/link"; +import { useState, useEffect } from "react"; +import Page from "../components/page"; -import { queryLLM, getStampyContent, runSearch } from "../hooks/useSearch"; -import type { Mode, Entry, LLMSettings } from "../types"; -import Header from "../components/header"; +import useSettings from "../hooks/useSettings"; +import type { Entry } from "../types"; 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 } from "../components/settings"; const Playground: NextPage = () => { const [sessionId, setSessionId] = useState(""); - const [settings, updateSettings] = useState(DEFAULT_SETTINGS); const [query, setQuery] = useState(""); const [history, setHistory] = useState([]); - - const setMode = (mode: [Mode, boolean]) => { - if (mode[1]) { - localStorage.setItem("chat_mode", mode[0]); - updateSettings((settings) => ({ ...settings, mode: mode[0] })); - } - }; + const { settings, changeSetting, setMode } = useSettings(); // initial load useEffect(() => { - const mode = (localStorage.getItem("chat_mode") as Mode) || "default"; - setMode([mode, true]); setSessionId(crypto.randomUUID()); }, []); return ( - <> - - AI Safety Info - -
-
- -
- - - -
-
- + + +
+ + + +
+
); }; diff --git a/web/src/settings.ts b/web/src/settings.ts index 5dd7442..e1aa14a 100644 --- a/web/src/settings.ts +++ b/web/src/settings.ts @@ -3,3 +3,27 @@ export const API_URL = export const STAMPY_URL = process.env.STAMPY_URL || "https://aisafety.info"; export const STAMPY_CONTENT_URL = process.env.STAMPY_CONTENT_URL || `${API_URL}/human`; + +// initial questions to fill the search box with. +export const initialQuestions: string[] = [ + "Are there any regulatory efforts aimed at addressing AI safety and alignment concerns?", + "How can I help with AI safety and alignment?", + "How could a predictive model - like an LLM - act like an agent?", + "How could an AI possibly be an x-risk when some populations aren't even connected to the internet?", + "I'm not convinced, why is this important?", + "Summarize the differences in opinion between Eliezer Yudkowsky and Paul Christiano.", + 'What are "RAAPs"?', + 'What are "scaling laws" and how are they relevant to safety?', + "What are some of the different research approaches?", + "What are the differences between Inner and Outer alignment?", + 'What does the term "x-risk" mean?', + 'What is "FOOM"?', + 'What is "instrumental convergence"?', + "What is a hard takeoff?", + "What is a mesa-optimizer?", + "What is AI safety and alignment?", + "What is an AI arms race?", + "What is an Intelligence Explosion?", + 'What is the "orthogonality thesis"?', + 'Why would we expect AI to be "misaligned by default"?', +]; diff --git a/web/src/types.ts b/web/src/types.ts index aa99fc1..e751c66 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -23,7 +23,7 @@ export type UserEntry = { export type AssistantEntry = { role: "assistant"; content: string; - citations: Citation[]; + citations?: Citation[]; citationsMap: Map; deleted?: boolean; }; @@ -64,3 +64,5 @@ export type LLMSettings = { contextFraction?: number; [key: string]: any; }; + +export type Parseable = string | number | undefined;