Merge pull request #121 from StampyAI/parametrized-url

LLM settings from url
This commit is contained in:
Daniel O'Connell
2023-10-25 08:59:06 +02:00
committed by GitHub
16 changed files with 667 additions and 559 deletions
+4 -5
View File
@@ -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')
+7 -2
View File
@@ -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,
+43 -65
View File
@@ -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 <p>Loading: Sending query...</p>;
case "semantic":
return <p>Loading: Performing semantic search...</p>;
case "context":
return <p>Loading: Creating context...</p>;
case "prompt":
return <p>Loading: Creating prompt...</p>;
case "llm":
return <p>Loading: Waiting for LLM...</p>;
case "streaming":
return <AssistantEntry entry={current} />;
case "followups":
return (
<>
<AssistantEntry entry={current} />
<p>Checking for followups...</p>
</>
);
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<Entry[]>([]);
const [current, setCurrent] = useState<CurrentSearch>();
const [citations, setCitations] = useState<Citation[]>([]);
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 = <p>Loading: Performing semantic search...</p>;
break;
case "prompt":
last_entry = <p>Loading: Creating prompt...</p>;
break;
case "llm":
last_entry = <p>Loading: Waiting for LLM...</p>;
break;
case "streaming":
updateCitations(citations, current);
last_entry = <AssistantEntry entry={current} />;
break;
case "followups":
last_entry = (
<>
<AssistantEntry entry={current} />
<p>Checking for followups...</p>
</>
);
break;
default:
last_entry = (
<button onClick={() => setEntries([])}>Clear history</button>
);
break;
}
return (
<ul className="flex-auto">
{entries.map(
@@ -185,8 +159,12 @@ const Chat = ({ sessionId, settings, onQuery, onNewEntry }: ChatParams) => {
)
)}
<SearchBox search={search} onQuery={onQuery} />
{last_entry}
<ChatResponse
current={current}
defaultElem={
<button onClick={() => setEntries([])}>Clear history</button>
}
/>
</ul>
);
};
+28 -46
View File
@@ -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 (
<div className="ml-auto mr-0 mb-5 flex w-fit flex-row justify-center gap-2">
<button
className={
"border border-gray-300 px-1 " +
(mode[1] && mode[0] === "rookie" ? "bg-gray-200" : "")
}
onClick={() => {
setMode(["rookie", true]);
}}
title="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."
>
rookie
</button>
<button
className={
"border border-gray-300 px-1 " +
(mode[1] && mode[0] === "concise" ? "bg-gray-200" : "")
}
onClick={() => {
setMode(["concise", true]);
}}
title="Quick and to the point. Followup questions may need to be
asked to get the full picture of what's going on."
>
concise
</button>
<button
className={
"border border-gray-300 px-1 " +
(mode[1] && mode[0] === "default" ? "bg-gray-200" : "")
}
onClick={() => {
setMode(["default", true]);
}}
title="A balanced default mode."
>
default
</button>
{Object.entries(MODES).map(([modeType, title]) => (
<button
className={
"border border-gray-300 px-1 " +
(mode === modeType ? "bg-gray-200" : "")
}
onClick={() => setMode(modeType)}
title={title}
key={modeType}
>
{modeType}
</button>
))}
</div>
);
};
+3 -3
View File
@@ -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" ? (
<span className="flex flex-1 flex-col justify-start text-right font-semibold">
+89
View File
@@ -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<{
<a className={className}>{children}</a>
);
};
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 }) => (
<h4 className="col-span-4 text-lg font-semibold">{text}</h4>
);
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) => (
<>
<label htmlFor={field} className="col-span-3 inline-block">
{label}:{" "}
</label>
<input
name={field}
value={value}
className="w-20"
onChange={between(min, max, parser, updater)}
type="number"
/>
</>
);
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) => (
<>
<label htmlFor={field} className="col-span-2">
{label}:
</label>
<input
name={field}
className="col-span-2"
value={value}
onChange={between(min, max, parser, updater)}
type="range"
min={min}
max={max}
step={step}
/>
</>
);
+7 -5
View File
@@ -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 (
<>
<Head>
<title>AI Safety Info</title>
</Head>
<main>
<main style={widescreen ? { maxWidth: "none" } : {}}>
<Header page={page} />
{children}
</main>
+1 -24
View File
@@ -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,
+201
View File
@@ -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 (
<div
className="chat-settings mx-5 grid w-[400px] flex-none grid-cols-4 gap-4 border-2 outline-black"
style={{ height: "fit-content" }}
>
<SectionHeader text="Models" />
<label htmlFor="completions-model" className="col-span-2">
Completions model:
</label>
<select
name="completions-model"
className="col-span-2"
value={settings.completions}
onChange={(event: ChangeEvent) => {
const value = (event.target as HTMLInputElement).value;
const { maxNumTokens, topKBlocks } =
MODELS[value as keyof typeof MODELS];
const prevNumTokens =
MODELS[settings.completions as keyof typeof MODELS].maxNumTokens;
const prevTopKBlocks =
MODELS[settings.completions as keyof typeof MODELS].topKBlocks;
if (settings.maxNumTokens === prevNumTokens) {
changeVal("maxNumTokens", maxNumTokens);
} else {
changeVal(
"maxNumTokens",
Math.min(settings.maxNumTokens || 0, maxNumTokens)
);
}
if (settings.topKBlocks === prevTopKBlocks) {
changeVal("topKBlocks", topKBlocks);
}
changeVal("completions", value);
}}
>
{Object.keys(MODELS).map((name) => (
<option value={name} key={name}>
{name}
</option>
))}
</select>
<label htmlFor="encoder" className="col-span-2">
Encoder:
</label>
<select
name="encoder"
className="col-span-2"
value={settings.encoder}
onChange={update("encoder")}
>
{ENCODERS.map((name) => (
<option value={name} key={name}>
{name}
</option>
))}
</select>
<SectionHeader text="Token options" />
<NumberInput
value={settings.maxNumTokens}
field="maxNumTokens"
label="Tokens"
min="1"
max={MODELS[settings.completions as keyof typeof MODELS].maxNumTokens}
updater={updateNum("maxNumTokens")}
/>
<NumberInput
field="tokensBuffer"
value={settings.tokensBuffer}
label="Number of tokens to leave as a buffer when calculating remaining tokens"
min="0"
max={settings.maxNumTokens}
updater={updateNum("tokensBuffer")}
/>
<SectionHeader text="Prompt options" />
<NumberInput
value={settings.topKBlocks}
field="topKBlocks"
label="Number of blocks to use as citations"
min="1"
updater={updateNum("topKBlocks")}
/>
<NumberInput
value={settings.maxHistory}
field="maxHistory"
label="The max number of previous interactions to use"
min="0"
updater={updateNum("maxHistory")}
/>
<Slider
value={settings.contextFraction}
field="contextFraction"
label="Approximate fraction of num_tokens to use for citations text before truncating"
updater={updateNum("contextFraction")}
/>
<Slider
value={settings.historyFraction}
field="historyFraction"
label="Approximate fraction of num_tokens to use for history text before truncating"
updater={updateNum("historyFraction")}
/>
</div>
);
};
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 (
<div className="chat-prompts mx-5 w-[400px] flex-none border-2 p-5 outline-black">
<details open>
<summary>Source prompt</summary>
<TextareaAutosize
className="border-gray w-full border px-1"
value={settings?.prompts?.context}
onChange={updatePrompt("context")}
/>
<div>(This is where sources will be injected)</div>
</details>
{history.length > 0 && (
<details open>
<summary>History prompt</summary>
<TextareaAutosize
className="border-gray w-full border px-1"
value={settings?.prompts?.history}
onChange={updatePrompt("history")}
/>
<details>
<summary>History</summary>
{history
.slice(Math.max(0, history.length - (settings.maxHistory || 0)))
.map((entry, i) => (
<div className="history-entry" key={i}>
{entry.content}
</div>
))}
</details>
</details>
)}
<details open>
<summary>Question prompt</summary>
<TextareaAutosize
className="border-gray w-full border px-1"
value={settings?.prompts?.question}
onChange={updatePrompt("question")}
/>
<TextareaAutosize
className="border-gray w-full border px-1"
value={settings?.prompts?.modes[settings.mode || "default"]}
onChange={updatePrompt("modes", settings.mode || "default")}
/>
</details>
<div>Q: {query}</div>
</div>
);
};
+47
View File
@@ -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<Citation[]>([]);
const setEntryCitations = (entry: CurrentSearch) =>
updateCitations(citations, setCitations, entry);
return {
citations,
setEntryCitations,
};
}
+10 -6
View File
@@ -57,16 +57,19 @@ export async function* iterateData(res: Response) {
}
}
export const extractAnswer = async (
res: Response,
setCurrent: (e: CurrentSearch) => void
): Promise<SearchResult> => {
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<SearchResult> => {
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<SearchResult> => {
setCurrent({ ...makeEntry(), phase: "started" });
// do SSE on a POST request.
const res = await fetchLLM(sessionId, query, settings, history, controller);
+172
View File
@@ -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<LLMSettings>(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,
};
}
+4 -10
View File
@@ -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 (
<Page page="index">
<Controls mode={mode} setMode={setMode} />
<Controls mode={settings.mode || "default"} setMode={setMode} />
<h2 className="bg-red-100 text-red-800">
<b>WARNING</b>: This is a very <b>early prototype</b>.{" "}
@@ -38,7 +32,7 @@ const Home: NextPage = () => {
welcomed.
</h2>
<Chat sessionId={sessionId} settings={{ mode: mode[0] }} />
<Chat sessionId={sessionId} settings={{ mode: settings.mode }} />
</Page>
);
};
+24 -392
View File
@@ -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 }) => (
<h4 className="col-span-4 text-lg font-semibold">{text}</h4>
);
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) => (
<>
<label htmlFor={field} className="col-span-3 inline-block">
{label}:{" "}
</label>
<input
name={field}
value={value}
className="w-20"
onChange={between(min, max, parser, updater)}
type="number"
/>
</>
);
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) => (
<>
<label htmlFor={field} className="col-span-2">
{label}:
</label>
<input
name={field}
className="col-span-2"
value={value}
onChange={between(min, max, parser, updater)}
type="range"
min={min}
max={max}
step={step}
/>
</>
);
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 (
<div
className="chat-settings mx-5 grid w-[400px] flex-none grid-cols-4 gap-4 border-2 outline-black"
style={{ height: "fit-content" }}
>
<SectionHeader text="Models" />
<label htmlFor="completions-model" className="col-span-2">
Completions model:
</label>
<select
name="completions-model"
className="col-span-2"
value={settings.completions}
onChange={(event: ChangeEvent) => {
const value = (event.target as HTMLInputElement).value;
const { maxNumTokens, topKBlocks } =
MODELS[value as keyof typeof MODELS];
const prevNumTokens =
MODELS[settings.completions as keyof typeof MODELS].maxNumTokens;
const prevTopKBlocks =
MODELS[settings.completions as keyof typeof MODELS].topKBlocks;
if (settings.maxNumTokens === prevNumTokens) {
changeVal("maxNumTokens", maxNumTokens);
} else {
changeVal(
"maxNumTokens",
Math.min(settings.maxNumTokens || 0, maxNumTokens)
);
}
if (settings.topKBlocks === prevTopKBlocks) {
changeVal("topKBlocks", topKBlocks);
}
changeVal("completions", value);
}}
>
{Object.keys(MODELS).map((name) => (
<option value={name} key={name}>
{name}
</option>
))}
</select>
<label htmlFor="encoder" className="col-span-2">
Encoder:
</label>
<select
name="encoder"
className="col-span-2"
value={settings.encoder}
onChange={update("encoder")}
>
{ENCODERS.map((name) => (
<option value={name} key={name}>
{name}
</option>
))}
</select>
<SectionHeader text="Token options" />
<NumberInput
value={settings.maxNumTokens}
field="maxNumTokens"
label="Tokens"
min="1"
max={MODELS[settings.completions as keyof typeof MODELS].maxNumTokens}
updater={updateNum("maxNumTokens")}
/>
<NumberInput
field="tokensBuffer"
value={settings.tokensBuffer}
label="Number of tokens to leave as a buffer when calculating remaining tokens"
min="0"
max={settings.maxNumTokens}
updater={updateNum("tokensBuffer")}
/>
<SectionHeader text="Prompt options" />
<NumberInput
value={settings.topKBlocks}
field="topKBlocks"
label="Number of blocks to use as citations"
min="1"
updater={updateNum("topKBlocks")}
/>
<NumberInput
value={settings.maxHistory}
field="maxHistory"
label="The max number of previous interactions to use"
min="0"
updater={updateNum("maxHistory")}
/>
<Slider
value={settings.contextFraction}
field="contextFraction"
label="Approximate fraction of num_tokens to use for citations text before truncating"
updater={updateNum("contextFraction")}
/>
<Slider
value={settings.historyFraction}
field="historyFraction"
label="Approximate fraction of num_tokens to use for history text before truncating"
updater={updateNum("historyFraction")}
/>
</div>
);
};
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 (
<div className="chat-prompts mx-5 w-[400px] flex-none border-2 p-5 outline-black">
<details open>
<summary>Source prompt</summary>
<TextareaAutosize
className="border-gray w-full border px-1"
value={settings?.prompts?.context}
onChange={updatePrompt("context")}
/>
<div>(This is where sources will be injected)</div>
</details>
{history.length > 0 && (
<details open>
<summary>History prompt</summary>
<TextareaAutosize
className="border-gray w-full border px-1"
value={settings?.prompts?.history}
onChange={updatePrompt("history")}
/>
<details>
<summary>History</summary>
{history
.slice(Math.max(0, history.length - (settings.maxHistory || 0)))
.map((entry, i) => (
<div className="history-entry" key={i}>
{entry.content}
</div>
))}
</details>
</details>
)}
<details open>
<summary>Question prompt</summary>
<TextareaAutosize
className="border-gray w-full border px-1"
value={settings?.prompts?.question}
onChange={updatePrompt("question")}
/>
<TextareaAutosize
className="border-gray w-full border px-1"
value={settings?.prompts?.modes[settings.mode || "default"]}
onChange={updatePrompt("modes", settings.mode || "default")}
/>
</details>
<div>Q: {query}</div>
</div>
);
};
import { ChatSettings, ChatPrompts } from "../components/settings";
const Playground: NextPage = () => {
const [sessionId, setSessionId] = useState("");
const [settings, updateSettings] = useState<LLMSettings>(DEFAULT_SETTINGS);
const [query, setQuery] = useState<string>("");
const [history, setHistory] = useState<Entry[]>([]);
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 (
<>
<Head>
<title>AI Safety Info</title>
</Head>
<main style={{ maxWidth: "none" }}>
<Header page="playground" />
<Controls mode={[settings.mode || "default", true]} setMode={setMode} />
<div className="flex">
<ChatPrompts
settings={settings}
query={query}
history={history}
updateSettings={updateSettings}
/>
<Chat
sessionId={sessionId}
settings={settings}
onQuery={setQuery}
onNewEntry={setHistory}
/>
<ChatSettings settings={settings} updateSettings={updateSettings} />
</div>
</main>
</>
<Page page="playground" widescreen={true}>
<Controls mode={settings.mode || "default"} setMode={setMode} />
<div className="flex">
<ChatPrompts
settings={settings}
query={query}
history={history}
changeSetting={changeSetting}
/>
<Chat
sessionId={sessionId}
settings={settings}
onQuery={setQuery}
onNewEntry={setHistory}
/>
<ChatSettings settings={settings} changeSetting={changeSetting} />
</div>
</Page>
);
};
+24
View File
@@ -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"?',
];
+3 -1
View File
@@ -23,7 +23,7 @@ export type UserEntry = {
export type AssistantEntry = {
role: "assistant";
content: string;
citations: Citation[];
citations?: Citation[];
citationsMap: Map<string, Citation>;
deleted?: boolean;
};
@@ -64,3 +64,5 @@ export type LLMSettings = {
contextFraction?: number;
[key: string]: any;
};
export type Parseable = string | number | undefined;