mirror of
https://github.com/wassname/stampy-chat.git
synced 2026-09-10 12:40:44 +08:00
Hooks for settings and citations
This commit is contained in:
+43
-65
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,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">
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -2,134 +2,9 @@ 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 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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,84 +1,44 @@
|
||||
import type { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState, useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
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";
|
||||
import {
|
||||
ChatSettings,
|
||||
ChatPrompts,
|
||||
updateIn,
|
||||
makeSettings,
|
||||
} from "../components/settings";
|
||||
import { ChatSettings, ChatPrompts } from "../components/settings";
|
||||
|
||||
const Playground: NextPage = () => {
|
||||
const [sessionId, setSessionId] = useState("");
|
||||
const [settings, updateSettings] = useState<LLMSettings>(makeSettings({}));
|
||||
|
||||
const [query, setQuery] = useState<string>("");
|
||||
const [history, setHistory] = useState<Entry[]>([]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const setMode = (mode: [Mode, boolean]) => {
|
||||
if (mode[1]) {
|
||||
localStorage.setItem("chat_mode", mode[0]);
|
||||
updateSettings((settings) => ({ ...settings, mode: mode[0] }));
|
||||
}
|
||||
};
|
||||
|
||||
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) }));
|
||||
};
|
||||
const { settings, changeSetting, setMode } = useSettings();
|
||||
|
||||
// initial load
|
||||
useEffect(() => {
|
||||
const mode = (localStorage.getItem("chat_mode") as Mode) || "default";
|
||||
updateSettings(makeSettings(router.query));
|
||||
setMode([mode, true]);
|
||||
setSessionId(crypto.randomUUID());
|
||||
}, [updateSettings, router]);
|
||||
}, []);
|
||||
|
||||
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}
|
||||
changeSetting={changeSetting}
|
||||
/>
|
||||
<Chat
|
||||
sessionId={sessionId}
|
||||
settings={settings}
|
||||
onQuery={setQuery}
|
||||
onNewEntry={setHistory}
|
||||
/>
|
||||
<ChatSettings settings={settings} changeSetting={changeSetting} />
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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"?',
|
||||
];
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ export type UserEntry = {
|
||||
export type AssistantEntry = {
|
||||
role: "assistant";
|
||||
content: string;
|
||||
citations: Citation[];
|
||||
citations?: Citation[];
|
||||
citationsMap: Map<string, Citation>;
|
||||
deleted?: boolean;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user