mirror of
https://github.com/wassname/stampy-chat.git
synced 2026-09-09 11:36:23 +08:00
Merge pull request #121 from StampyAI/parametrized-url
LLM settings from url
This commit is contained in:
+4
-5
@@ -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')
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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,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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+24
-392
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user