mirror of
https://github.com/wassname/stampy-chat.git
synced 2026-09-09 11:36:23 +08:00
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')
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
import { ChangeEvent } from "react";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
|
||||
import type { Parseable, LLMSettings, Entry, Mode } from "../types";
|
||||
import { SectionHeader, NumberInput, Slider } from "../components/html";
|
||||
|
||||
type LLMSettingsParsers = {
|
||||
[key: string]:
|
||||
| ((v: number | undefined) => any)
|
||||
| ((v: string | undefined) => any)
|
||||
| ((v: object | undefined) => any);
|
||||
};
|
||||
|
||||
const DEFAULT_PROMPTS = {
|
||||
context:
|
||||
"You are a helpful assistant knowledgeable about AI Alignment and Safety. " +
|
||||
'Please give a clear and coherent answer to the user\'s questions.(written after "Q:") ' +
|
||||
"using the following sources. Each source is labeled with a letter. Feel free to " +
|
||||
"use the sources in any order, and try to use multiple sources in your answers.\n\n",
|
||||
history:
|
||||
"\n\n" +
|
||||
'Before the question ("Q: "), there will be a history of previous questions and answers. ' +
|
||||
"These sources only apply to the last question. any sources used in previous answers " +
|
||||
"are invalid.",
|
||||
question:
|
||||
"In your answer, please cite any claims you make back to each source " +
|
||||
"using the format: [a], [b], etc. If you use multiple sources to make a claim " +
|
||||
'cite all of them. For example: "AGI is concerning [c, d, e]."\n\n',
|
||||
modes: {
|
||||
default: "",
|
||||
concise:
|
||||
"Answer very concisely, getting to the crux of the matter in as " +
|
||||
"few words as possible. Limit your answer to 1-2 sentences.\n\n",
|
||||
rookie:
|
||||
"This user is new to the field of AI Alignment and Safety - don't " +
|
||||
"assume they know any technical terms or jargon. Still give a complete answer " +
|
||||
"without patronizing the user, but take any extra time needed to " +
|
||||
"explain new concepts or to illustrate your answer with examples. " +
|
||||
"Put extra effort into explaining the intuition behind concepts " +
|
||||
"rather than just giving a formal definition.\n\n",
|
||||
},
|
||||
};
|
||||
export const MODELS = {
|
||||
"gpt-3.5-turbo": { maxNumTokens: 4095, topKBlocks: 10 },
|
||||
"gpt-3.5-turbo-16k": { maxNumTokens: 16385, topKBlocks: 30 },
|
||||
"gpt-4": { maxNumTokens: 8192, topKBlocks: 20 },
|
||||
/* 'gpt-4-32k': {maxNumTokens: 32768, topKBlocks: 30}, */
|
||||
};
|
||||
export const ENCODERS = ["cl100k_base"];
|
||||
|
||||
/** Update the given `obj` so that it has `val` at the given path.
|
||||
*
|
||||
* e.g.
|
||||
* updateIn({a: {b: 123}}, ['a', 'b', 'c'], 42) == {a: {b: 123, c: 42}}
|
||||
* updateIn({a: {b: 123}}, ['z', 'y', 'x'], 42) == {a: {b: 123}, z: {y: {x: 42}}}
|
||||
*/
|
||||
export const updateIn = (
|
||||
obj: { [key: string]: any },
|
||||
[head, ...rest]: string[],
|
||||
val: any
|
||||
) => {
|
||||
if (!head) {
|
||||
// No path provided - do nothing
|
||||
} else if (!rest || rest.length == 0) {
|
||||
obj[head] = val;
|
||||
} else {
|
||||
if (obj[head] === undefined) {
|
||||
obj[head] = {};
|
||||
}
|
||||
updateIn(obj[head], rest, val);
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
/** Create a settings object in which all items in the `overrides` object will be parsed appropriately
|
||||
*
|
||||
* `parsers` should be an object mapping settings fields to functions that will return a valid setting.
|
||||
* The parser functions should have default values that will be used if the provided value is undefined.
|
||||
*/
|
||||
const parseSettings = (overrides: LLMSettings, parsers: LLMSettingsParsers) =>
|
||||
Object.entries(parsers).reduce(
|
||||
(settings, [key, parser]) =>
|
||||
updateIn(settings, [key], parser(overrides[key])),
|
||||
{}
|
||||
);
|
||||
|
||||
/** Make a parser function from the provided `defaultVal`.
|
||||
*
|
||||
* If the parsed value is undefined, `defaultVal` will be returned, otherwise it will be parsed as
|
||||
* a value of the same type as `defaultVal`.
|
||||
* If `defaultVal` is an object, it will return a parser that will recursively search for appropriate keys.
|
||||
*/
|
||||
const withDefault = (defaultVal: any) => {
|
||||
if (typeof defaultVal === "number" && defaultVal % 1 === 0) {
|
||||
return (v: string | undefined): number =>
|
||||
v !== undefined ? parseInt(v, 10) : defaultVal;
|
||||
} else if (typeof defaultVal === "number") {
|
||||
return (v: string | undefined): number =>
|
||||
v !== undefined ? parseFloat(v) : defaultVal;
|
||||
} else if (typeof defaultVal === "object") {
|
||||
const parsers = Object.entries(defaultVal).reduce(
|
||||
(parsers, [key, val]) => updateIn(parsers, [key], withDefault(val)),
|
||||
{}
|
||||
);
|
||||
return (v: object | undefined): object => parseSettings(v || {}, parsers);
|
||||
} else {
|
||||
return (v: any | undefined): any => v || defaultVal;
|
||||
}
|
||||
};
|
||||
|
||||
const SETTINGS_PARSERS = {
|
||||
prompts: withDefault(DEFAULT_PROMPTS),
|
||||
mode: (v: string | undefined) => (v || "default") as Mode,
|
||||
completions: withDefault("gpt-3.5-turbo"),
|
||||
encoder: withDefault("cl100k_base"),
|
||||
topKBlocks: withDefault(MODELS["gpt-3.5-turbo"].topKBlocks), // the number of blocks to use as citations
|
||||
maxNumTokens: withDefault(MODELS["gpt-3.5-turbo"].maxNumTokens),
|
||||
tokensBuffer: withDefault(50), // the number of tokens to leave as a buffer when calculating remaining tokens
|
||||
maxHistory: withDefault(10), // the max number of previous items to use as history
|
||||
historyFraction: withDefault(0.25), // the (approximate) fraction of num_tokens to use for history text before truncating
|
||||
contextFraction: withDefault(0.5), // the (approximate) fraction of num_tokens to use for context text before truncating
|
||||
};
|
||||
|
||||
export const makeSettings = (overrides: LLMSettings) =>
|
||||
parseSettings(
|
||||
Object.entries(overrides).reduce(
|
||||
(acc, [key, val]) => updateIn(acc, key.split("."), val),
|
||||
{}
|
||||
),
|
||||
SETTINGS_PARSERS
|
||||
);
|
||||
|
||||
type ChatSettingsParams = {
|
||||
settings: LLMSettings;
|
||||
changeSetting: (path: string[], value: any) => void;
|
||||
};
|
||||
|
||||
export const ChatSettings = ({
|
||||
settings,
|
||||
changeSetting,
|
||||
}: ChatSettingsParams) => {
|
||||
const changeVal = (field: string, value: any) =>
|
||||
changeSetting([field], value);
|
||||
const update = (field: string) => (event: ChangeEvent) =>
|
||||
changeVal(field, (event.target as HTMLInputElement).value);
|
||||
const updateNum = (field: string) => (num: Parseable) =>
|
||||
changeVal(field, num);
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
+32
-357
@@ -1,373 +1,29 @@
|
||||
import type { NextPage } from "next";
|
||||
import { useState, useEffect, ChangeEvent } from "react";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState, useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
|
||||
import { queryLLM, getStampyContent, runSearch } from "../hooks/useSearch";
|
||||
import type { Mode, Entry, LLMSettings } from "../types";
|
||||
import Header from "../components/header";
|
||||
import Chat from "../components/chat";
|
||||
import { Controls } from "../components/controls";
|
||||
|
||||
const MAX_FOLLOWUPS = 4;
|
||||
const DEFAULT_PROMPTS = {
|
||||
context:
|
||||
"You are a helpful assistant knowledgeable about AI Alignment and Safety. " +
|
||||
'Please give a clear and coherent answer to the user\'s questions.(written after "Q:") ' +
|
||||
"using the following sources. Each source is labeled with a letter. Feel free to " +
|
||||
"use the sources in any order, and try to use multiple sources in your answers.\n\n",
|
||||
history:
|
||||
"\n\n" +
|
||||
'Before the question ("Q: "), there will be a history of previous questions and answers. ' +
|
||||
"These sources only apply to the last question. Any sources used in previous answers " +
|
||||
"are invalid.",
|
||||
question:
|
||||
"In your answer, please cite any claims you make back to each source " +
|
||||
"using the format: [a], [b], etc. If you use multiple sources to make a claim " +
|
||||
'cite all of them. For example: "AGI is concerning [c, d, e]."\n\n',
|
||||
modes: {
|
||||
default: "",
|
||||
concise:
|
||||
"Answer very concisely, getting to the crux of the matter in as " +
|
||||
"few words as possible. Limit your answer to 1-2 sentences.\n\n",
|
||||
rookie:
|
||||
"This user is new to the field of AI Alignment and Safety - don't " +
|
||||
"assume they know any technical terms or jargon. Still give a complete answer " +
|
||||
"without patronizing the user, but take any extra time needed to " +
|
||||
"explain new concepts or to illustrate your answer with examples. " +
|
||||
"Put extra effort into explaining the intuition behind concepts " +
|
||||
"rather than just giving a formal definition.\n\n",
|
||||
},
|
||||
};
|
||||
const MODELS = {
|
||||
"gpt-3.5-turbo": { maxNumTokens: 4095, topKBlocks: 10 },
|
||||
"gpt-3.5-turbo-16k": { maxNumTokens: 16385, topKBlocks: 30 },
|
||||
"gpt-4": { maxNumTokens: 8192, topKBlocks: 20 },
|
||||
/* 'gpt-4-32k': {maxNumTokens: 32768, topKBlocks: 30}, */
|
||||
};
|
||||
const DEFAULT_SETTINGS = {
|
||||
prompts: DEFAULT_PROMPTS,
|
||||
mode: "default" as Mode,
|
||||
completions: "gpt-3.5-turbo",
|
||||
encoder: "cl100k_base",
|
||||
topKBlocks: MODELS["gpt-3.5-turbo"].topKBlocks, // the number of blocks to use as citations
|
||||
maxNumTokens: MODELS["gpt-3.5-turbo"].maxNumTokens,
|
||||
tokensBuffer: 50, // the number of tokens to leave as a buffer when calculating remaining tokens
|
||||
maxHistory: 10, // the max number of previous items to use as history
|
||||
historyFraction: 0.25, // the (approximate) fraction of num_tokens to use for history text before truncating
|
||||
contextFraction: 0.5, // the (approximate) fraction of num_tokens to use for context text before truncating
|
||||
};
|
||||
const ENCODERS = ["cl100k_base"];
|
||||
|
||||
const updateIn = (
|
||||
obj: { [key: string]: any },
|
||||
[head, ...rest]: string[],
|
||||
val: any
|
||||
) => {
|
||||
if (!head) {
|
||||
// No path provided - do nothing
|
||||
} else if (!rest || rest.length == 0) {
|
||||
obj[head] = val;
|
||||
} else {
|
||||
updateIn(obj[head], rest, val);
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
type Parseable = string | number | undefined;
|
||||
type NumberParser = (v: Parseable) => number;
|
||||
type InputFields = {
|
||||
field: string;
|
||||
label: string;
|
||||
value?: Parseable;
|
||||
min?: string | number;
|
||||
max?: string | number;
|
||||
step?: string | number;
|
||||
parser?: NumberParser;
|
||||
updater: (v: any) => any;
|
||||
};
|
||||
|
||||
const between =
|
||||
(
|
||||
min: Parseable,
|
||||
max: Parseable,
|
||||
parser: NumberParser,
|
||||
updater: (v: any) => any
|
||||
) =>
|
||||
(event: ChangeEvent) => {
|
||||
let num = parser((event.target as HTMLInputElement).value);
|
||||
if (isNaN(num)) {
|
||||
return;
|
||||
} else if (min !== undefined && num < parser(min)) {
|
||||
num = parser(min);
|
||||
} else if (max !== undefined && num > parser(max)) {
|
||||
num = parser(max);
|
||||
}
|
||||
updater(num);
|
||||
};
|
||||
|
||||
const SectionHeader = ({ text }: { text: string }) => (
|
||||
<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,
|
||||
updateIn,
|
||||
makeSettings,
|
||||
} from "../components/settings";
|
||||
|
||||
const Playground: NextPage = () => {
|
||||
const [sessionId, setSessionId] = useState("");
|
||||
const [settings, updateSettings] = useState<LLMSettings>(DEFAULT_SETTINGS);
|
||||
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]);
|
||||
@@ -375,6 +31,21 @@ const Playground: NextPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const changeSetting = (path: string[], value: any) => {
|
||||
router.replace(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
[path.join(".")]: value.toString(),
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{ scroll: false, shallow: true }
|
||||
);
|
||||
updateSettings((settings) => ({ ...updateIn(settings, path, value) }));
|
||||
};
|
||||
|
||||
// initial load
|
||||
useEffect(() => {
|
||||
const mode = (localStorage.getItem("chat_mode") as Mode) || "default";
|
||||
@@ -382,6 +53,10 @@ const Playground: NextPage = () => {
|
||||
setSessionId(crypto.randomUUID());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
updateSettings(makeSettings(router.query));
|
||||
}, [updateSettings, router]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -395,7 +70,7 @@ const Playground: NextPage = () => {
|
||||
settings={settings}
|
||||
query={query}
|
||||
history={history}
|
||||
updateSettings={updateSettings}
|
||||
changeSetting={changeSetting}
|
||||
/>
|
||||
<Chat
|
||||
sessionId={sessionId}
|
||||
@@ -403,7 +78,7 @@ const Playground: NextPage = () => {
|
||||
onQuery={setQuery}
|
||||
onNewEntry={setHistory}
|
||||
/>
|
||||
<ChatSettings settings={settings} updateSettings={updateSettings} />
|
||||
<ChatSettings settings={settings} changeSetting={changeSetting} />
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
|
||||
@@ -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