mirror of
https://github.com/wassname/stampy-chat.git
synced 2026-09-09 11:36:23 +08:00
prettier formatting
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import {useState} from 'react'
|
||||
import {ShowCitation, CitationsBlock} from './citations'
|
||||
import {GlossarySpan} from './glossary'
|
||||
import type {Citation, AssistantEntry as AssistantType} from '../types'
|
||||
import { useState } from "react";
|
||||
import { ShowCitation, CitationsBlock } from "./citations";
|
||||
import { GlossarySpan } from "./glossary";
|
||||
import type { Citation, AssistantEntry as AssistantType } from "../types";
|
||||
|
||||
export const AssistantEntry: React.FC<{entry: AssistantType}> = ({entry}) => {
|
||||
export const AssistantEntry: React.FC<{ entry: AssistantType }> = ({
|
||||
entry,
|
||||
}) => {
|
||||
return (
|
||||
<div className="mt-3 mb-8">
|
||||
{entry.content.split('\n').map((paragraph, i) => (
|
||||
{entry.content.split("\n").map((paragraph, i) => (
|
||||
<CitationsBlock
|
||||
key={i}
|
||||
text={paragraph}
|
||||
@@ -25,5 +27,5 @@ export const AssistantEntry: React.FC<{entry: AssistantType}> = ({entry}) => {
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
+95
-84
@@ -1,5 +1,5 @@
|
||||
import {useState, useEffect} from 'react'
|
||||
import {queryLLM, getStampyContent, runSearch} from '../hooks/useSearch'
|
||||
import { useState, useEffect } from "react";
|
||||
import { queryLLM, getStampyContent, runSearch } from "../hooks/useSearch";
|
||||
|
||||
import type {
|
||||
CurrentSearch,
|
||||
@@ -8,102 +8,111 @@ import type {
|
||||
AssistantEntry as AssistantEntryType,
|
||||
LLMSettings,
|
||||
Followup,
|
||||
} from '../types'
|
||||
import {SearchBox} from '../components/searchbox'
|
||||
import {AssistantEntry} from '../components/assistant'
|
||||
import {Entry as EntryTag} from '../components/entry'
|
||||
} from "../types";
|
||||
import { SearchBox } from "../components/searchbox";
|
||||
import { AssistantEntry } from "../components/assistant";
|
||||
import { Entry as EntryTag } from "../components/entry";
|
||||
|
||||
const MAX_FOLLOWUPS = 4
|
||||
const MAX_FOLLOWUPS = 4;
|
||||
|
||||
type State =
|
||||
| {
|
||||
state: 'idle'
|
||||
state: "idle";
|
||||
}
|
||||
| {
|
||||
state: 'loading'
|
||||
phase: 'semantic' | 'prompt' | 'llm'
|
||||
citations: Citation[]
|
||||
state: "loading";
|
||||
phase: "semantic" | "prompt" | "llm";
|
||||
citations: Citation[];
|
||||
}
|
||||
| {
|
||||
state: 'streaming'
|
||||
response: AssistantEntryType
|
||||
}
|
||||
state: "streaming";
|
||||
response: AssistantEntryType;
|
||||
};
|
||||
|
||||
// smooth-scroll to the bottom of the window if we're already less than 30% a screen away
|
||||
// note: finicky interaction with "smooth" - maybe fix later.
|
||||
function scroll30() {
|
||||
if (document.documentElement.scrollHeight - window.scrollY > window.innerHeight * 1.3) return
|
||||
window.scrollTo({top: document.body.scrollHeight, behavior: 'smooth'})
|
||||
if (
|
||||
document.documentElement.scrollHeight - window.scrollY >
|
||||
window.innerHeight * 1.3
|
||||
)
|
||||
return;
|
||||
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
|
||||
}
|
||||
|
||||
type ChatParams = {
|
||||
sessionId: string
|
||||
settings: LLMSettings
|
||||
onQuery?: (q: string) => any
|
||||
onNewEntry?: (history: Entry[]) => any
|
||||
}
|
||||
sessionId: string;
|
||||
settings: LLMSettings;
|
||||
onQuery?: (q: string) => any;
|
||||
onNewEntry?: (history: Entry[]) => any;
|
||||
};
|
||||
|
||||
const Chat = ({sessionId, settings, onQuery, onNewEntry}: ChatParams) => {
|
||||
const [entries, setEntries] = useState<Entry[]>([])
|
||||
const [current, setCurrent] = useState<CurrentSearch>()
|
||||
const [citations, setCitations] = useState<Citation[]>([])
|
||||
const Chat = ({ sessionId, settings, onQuery, onNewEntry }: ChatParams) => {
|
||||
const [entries, setEntries] = useState<Entry[]>([]);
|
||||
const [current, setCurrent] = useState<CurrentSearch>();
|
||||
const [citations, setCitations] = useState<Citation[]>([]);
|
||||
|
||||
const updateCurrent = (current: CurrentSearch) => {
|
||||
setCurrent(current)
|
||||
if (current?.phase === 'streaming') {
|
||||
scroll30()
|
||||
setCurrent(current);
|
||||
if (current?.phase === "streaming") {
|
||||
scroll30();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateCitations = (allCitations: Citation[], current?: CurrentSearch) => {
|
||||
if (!current) return
|
||||
const updateCitations = (
|
||||
allCitations: Citation[],
|
||||
current?: CurrentSearch
|
||||
) => {
|
||||
if (!current) return;
|
||||
|
||||
const entryCitations = Array.from(current.citationsMap.values())
|
||||
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
|
||||
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]))
|
||||
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]
|
||||
const hash = c.title + c.url;
|
||||
const index = citationsMapping[hash];
|
||||
if (!index) {
|
||||
c.index = allCitations.length + 1
|
||||
allCitations.push(c)
|
||||
c.index = allCitations.length + 1;
|
||||
allCitations.push(c);
|
||||
} else {
|
||||
c.index = index
|
||||
c.index = index;
|
||||
}
|
||||
})
|
||||
setCitations(allCitations)
|
||||
setCurrent(current)
|
||||
}
|
||||
});
|
||||
setCitations(allCitations);
|
||||
setCurrent(current);
|
||||
};
|
||||
|
||||
const addEntry = (entry: Entry) => {
|
||||
setEntries((prev) => {
|
||||
const entries = [...prev, entry]
|
||||
const entries = [...prev, entry];
|
||||
if (onNewEntry) {
|
||||
onNewEntry(entries)
|
||||
onNewEntry(entries);
|
||||
}
|
||||
return entries
|
||||
})
|
||||
}
|
||||
return entries;
|
||||
});
|
||||
};
|
||||
|
||||
const search = async (
|
||||
query: string,
|
||||
query_source: 'search' | 'followups',
|
||||
query_source: "search" | "followups",
|
||||
enable: (f_set: Followup[] | ((fs: Followup[]) => Followup[])) => void,
|
||||
controller: AbortController
|
||||
) => {
|
||||
// clear the query box, append to entries
|
||||
const userEntry: Entry = {
|
||||
role: 'user',
|
||||
content: query_source === 'search' ? query : query.split('\n', 2)[1]!,
|
||||
}
|
||||
role: "user",
|
||||
content: query_source === "search" ? query : query.split("\n", 2)[1]!,
|
||||
};
|
||||
|
||||
const {result, followups} = await runSearch(
|
||||
const { result, followups } = await runSearch(
|
||||
query,
|
||||
query_source,
|
||||
settings,
|
||||
@@ -111,44 +120,46 @@ const Chat = ({sessionId, settings, onQuery, onNewEntry}: ChatParams) => {
|
||||
updateCurrent,
|
||||
sessionId,
|
||||
controller
|
||||
)
|
||||
if (result.content !== 'aborted') {
|
||||
addEntry(userEntry)
|
||||
addEntry(result)
|
||||
enable(followups || [])
|
||||
scroll30()
|
||||
);
|
||||
if (result.content !== "aborted") {
|
||||
addEntry(userEntry);
|
||||
addEntry(result);
|
||||
enable(followups || []);
|
||||
scroll30();
|
||||
} else {
|
||||
enable([])
|
||||
enable([]);
|
||||
}
|
||||
setCurrent(undefined)
|
||||
}
|
||||
setCurrent(undefined);
|
||||
};
|
||||
|
||||
var last_entry = <></>
|
||||
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':
|
||||
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
|
||||
);
|
||||
break;
|
||||
default:
|
||||
last_entry = <button onClick={() => setEntries([])}>Clear history</button>
|
||||
break
|
||||
last_entry = (
|
||||
<button onClick={() => setEntries([])}>Clear history</button>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -160,7 +171,7 @@ const Chat = ({sessionId, settings, onQuery, onNewEntry}: ChatParams) => {
|
||||
|
||||
{last_entry}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Chat
|
||||
export default Chat;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type {Citation} from '../types'
|
||||
import {Colours, A} from './html'
|
||||
import type { Citation } from "../types";
|
||||
import { Colours, A } from "./html";
|
||||
|
||||
export const formatCitations: (text: string) => string = (text) => {
|
||||
// ---------------------- normalize citation form ----------------------
|
||||
@@ -13,10 +13,10 @@ export const formatCitations: (text: string) => string = (text) => {
|
||||
|
||||
(block: string) =>
|
||||
block
|
||||
.split(',')
|
||||
.split(",")
|
||||
.map((x) => x.trim())
|
||||
.join('][')
|
||||
)
|
||||
.join("][")
|
||||
);
|
||||
|
||||
// transform all things that look like [(a), (b), (c)] into [(a)][(b)][(c)]
|
||||
response = response.replace(
|
||||
@@ -24,107 +24,118 @@ export const formatCitations: (text: string) => string = (text) => {
|
||||
|
||||
(block: string) =>
|
||||
block
|
||||
.split(',')
|
||||
.split(",")
|
||||
.map((x) => x.trim())
|
||||
.join('][')
|
||||
)
|
||||
.join("][")
|
||||
);
|
||||
|
||||
// transform all things that look like [(a)] into [a]
|
||||
response = response.replace(/\[\(([a-z]+)\)\]/g, (_match: string, x: string) => `[${x}]`)
|
||||
response = response.replace(
|
||||
/\[\(([a-z]+)\)\]/g,
|
||||
(_match: string, x: string) => `[${x}]`
|
||||
);
|
||||
|
||||
// transform all things that look like [ a ] into [a]
|
||||
response = response.replace(/\[\s*([a-z]+)\s*\]/g, (_match: string, x: string) => `[${x}]`)
|
||||
return response
|
||||
}
|
||||
response = response.replace(
|
||||
/\[\s*([a-z]+)\s*\]/g,
|
||||
(_match: string, x: string) => `[${x}]`
|
||||
);
|
||||
return response;
|
||||
};
|
||||
|
||||
export const findCitations: (text: string, citations: Citation[]) => Map<string, Citation> = (
|
||||
text,
|
||||
citations
|
||||
) => {
|
||||
export const findCitations: (
|
||||
text: string,
|
||||
citations: Citation[]
|
||||
) => Map<string, Citation> = (text, citations) => {
|
||||
// figure out what citations are in the response, and map them appropriately
|
||||
const cite_map = new Map<string, Citation>()
|
||||
const cite_map = new Map<string, Citation>();
|
||||
|
||||
// scan a regex for [x] over the response. If x isn't in the map, add it.
|
||||
// (note: we're actually doing this twice - once on parsing, once on render.
|
||||
// if that looks like a problem, we could swap from strings to custom ropes).
|
||||
const regex = /\[([a-z]+)\]/g
|
||||
let match
|
||||
const regex = /\[([a-z]+)\]/g;
|
||||
let match;
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
const letter = match[1]
|
||||
if (!letter || cite_map.has(letter!)) continue
|
||||
const letter = match[1];
|
||||
if (!letter || cite_map.has(letter!)) continue;
|
||||
|
||||
const citation = citations[letter.charCodeAt(0) - 'a'.charCodeAt(0)]
|
||||
if (!citation) continue
|
||||
const citation = citations[letter.charCodeAt(0) - "a".charCodeAt(0)];
|
||||
if (!citation) continue;
|
||||
|
||||
cite_map.set(letter!, citation)
|
||||
cite_map.set(letter!, citation);
|
||||
}
|
||||
return cite_map
|
||||
}
|
||||
return cite_map;
|
||||
};
|
||||
|
||||
export const ShowCitation: React.FC<{citation: Citation}> = ({citation}) => {
|
||||
var c_str = citation.title
|
||||
export const ShowCitation: React.FC<{ citation: Citation }> = ({
|
||||
citation,
|
||||
}) => {
|
||||
var c_str = citation.title;
|
||||
|
||||
if (citation.authors && citation.authors.length > 0) c_str += ' - ' + citation.authors.join(', ')
|
||||
if (citation.date && citation.date !== '') c_str += ' - ' + citation.date
|
||||
if (citation.authors && citation.authors.length > 0)
|
||||
c_str += " - " + citation.authors.join(", ");
|
||||
if (citation.date && citation.date !== "") c_str += " - " + citation.date;
|
||||
|
||||
// if we don't have a url, link to a duckduckgo search for the title instead
|
||||
const url =
|
||||
citation.url && citation.url !== ''
|
||||
citation.url && citation.url !== ""
|
||||
? citation.url
|
||||
: `https://duckduckgo.com/?q=${encodeURIComponent(citation.title)}`
|
||||
: `https://duckduckgo.com/?q=${encodeURIComponent(citation.title)}`;
|
||||
|
||||
return (
|
||||
<A
|
||||
className={
|
||||
Colours[(citation.index - 1) % Colours.length] +
|
||||
' my-2 flex w-fit items-center rounded border-2 text-sm no-underline'
|
||||
" my-2 flex w-fit items-center rounded border-2 text-sm no-underline"
|
||||
}
|
||||
href={url}
|
||||
>
|
||||
<span className="mx-1"> [{citation.index}] </span>
|
||||
<p className="mx-1 my-0"> {c_str} </p>
|
||||
</A>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const CitationRef: React.FC<{citation?: Citation}> = ({citation}) => {
|
||||
if (!citation) return null
|
||||
export const CitationRef: React.FC<{ citation?: Citation }> = ({
|
||||
citation,
|
||||
}) => {
|
||||
if (!citation) return null;
|
||||
|
||||
const url =
|
||||
citation.url && citation.url !== ''
|
||||
citation.url && citation.url !== ""
|
||||
? citation.url
|
||||
: `https://duckduckgo.com/?q=${encodeURIComponent(citation.title)}`
|
||||
: `https://duckduckgo.com/?q=${encodeURIComponent(citation.title)}`;
|
||||
return (
|
||||
<A
|
||||
className={
|
||||
Colours[(citation.index - 1) % Colours.length] +
|
||||
' ml-1 mr-0.5 w-min rounded border-2 px-0.5 pb-0.5 text-sm no-underline'
|
||||
" ml-1 mr-0.5 w-min rounded border-2 px-0.5 pb-0.5 text-sm no-underline"
|
||||
}
|
||||
href={url}
|
||||
>
|
||||
[{citation.index}]
|
||||
</A>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const CitationsBlock: React.FC<{
|
||||
text: string
|
||||
citations: Map<string, Citation>
|
||||
textRenderer: (t: string) => any
|
||||
}> = ({text, citations, textRenderer}) => {
|
||||
const regex = /\[([a-z]+)\]/g
|
||||
text: string;
|
||||
citations: Map<string, Citation>;
|
||||
textRenderer: (t: string) => any;
|
||||
}> = ({ text, citations, textRenderer }) => {
|
||||
const regex = /\[([a-z]+)\]/g;
|
||||
return (
|
||||
<p>
|
||||
{' '}
|
||||
{" "}
|
||||
{text.split(regex).map((part, i) => {
|
||||
// When splitting, the even parts are basic text sections, while the odd ones are
|
||||
// citations
|
||||
if (i % 2 == 0) {
|
||||
return textRenderer(part)
|
||||
return textRenderer(part);
|
||||
} else {
|
||||
return <CitationRef citation={citations.get(part)} key={i} />
|
||||
return <CitationRef citation={citations.get(part)} key={i} />;
|
||||
}
|
||||
})}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type {Mode} from '../types'
|
||||
import type { Mode } from "../types";
|
||||
|
||||
export const Controls = ({mode, setMode}: {mode: [Mode, boolean]; setMode: (m: any) => void}) => {
|
||||
export const Controls = ({
|
||||
mode,
|
||||
setMode,
|
||||
}: {
|
||||
mode: [Mode, boolean];
|
||||
setMode: (m: any) => void;
|
||||
}) => {
|
||||
{
|
||||
/* three buttons for the three modes, place far right, 1rem between each */
|
||||
}
|
||||
@@ -8,10 +14,11 @@ export const Controls = ({mode, setMode}: {mode: [Mode, boolean]; setMode: (m: a
|
||||
<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' : '')
|
||||
"border border-gray-300 px-1 " +
|
||||
(mode[1] && mode[0] === "rookie" ? "bg-gray-200" : "")
|
||||
}
|
||||
onClick={() => {
|
||||
setMode(['rookie', true])
|
||||
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
|
||||
@@ -22,10 +29,11 @@ export const Controls = ({mode, setMode}: {mode: [Mode, boolean]; setMode: (m: a
|
||||
</button>
|
||||
<button
|
||||
className={
|
||||
'border border-gray-300 px-1 ' + (mode[1] && mode[0] === 'concise' ? 'bg-gray-200' : '')
|
||||
"border border-gray-300 px-1 " +
|
||||
(mode[1] && mode[0] === "concise" ? "bg-gray-200" : "")
|
||||
}
|
||||
onClick={() => {
|
||||
setMode(['concise', true])
|
||||
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."
|
||||
@@ -34,15 +42,16 @@ export const Controls = ({mode, setMode}: {mode: [Mode, boolean]; setMode: (m: a
|
||||
</button>
|
||||
<button
|
||||
className={
|
||||
'border border-gray-300 px-1 ' + (mode[1] && mode[0] === 'default' ? 'bg-gray-200' : '')
|
||||
"border border-gray-300 px-1 " +
|
||||
(mode[1] && mode[0] === "default" ? "bg-gray-200" : "")
|
||||
}
|
||||
onClick={() => {
|
||||
setMode(['default', true])
|
||||
setMode(["default", true]);
|
||||
}}
|
||||
title="A balanced default mode."
|
||||
>
|
||||
default
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,14 +4,14 @@ import type {
|
||||
ErrorMessage,
|
||||
StampyMessage,
|
||||
UserEntry,
|
||||
} from '../types'
|
||||
import {AssistantEntry} from './assistant'
|
||||
import {GlossarySpan} from './glossary'
|
||||
import Image from 'next/image'
|
||||
import logo from '../logo.svg'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
} from "../types";
|
||||
import { AssistantEntry } from "./assistant";
|
||||
import { GlossarySpan } from "./glossary";
|
||||
import Image from "next/image";
|
||||
import logo from "../logo.svg";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
|
||||
export const User = ({entry}: {entry: UserEntry}) => {
|
||||
export const User = ({ entry }: { entry: UserEntry }) => {
|
||||
return (
|
||||
<li className="mt-1 mb-2 flex">
|
||||
<TextareaAutosize
|
||||
@@ -19,59 +19,66 @@ export const User = ({entry}: {entry: UserEntry}) => {
|
||||
value={entry.content}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const Error = ({entry}: {entry: ErrorMessage}) => {
|
||||
export const Error = ({ entry }: { entry: ErrorMessage }) => {
|
||||
return (
|
||||
<li>
|
||||
<p className="border border-red-500 bg-red-100 px-1 text-red-800"> {entry.content} </p>
|
||||
<p className="border border-red-500 bg-red-100 px-1 text-red-800">
|
||||
{" "}
|
||||
{entry.content}{" "}
|
||||
</p>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const Assistant = ({entry}: {entry: AssistantEntryType}) => {
|
||||
export const Assistant = ({ entry }: { entry: AssistantEntryType }) => {
|
||||
return (
|
||||
<li>
|
||||
<AssistantEntry entry={entry} />
|
||||
</li>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const Stampy = ({entry}: {entry: StampyMessage}) => {
|
||||
export const Stampy = ({ entry }: { entry: StampyMessage }) => {
|
||||
return (
|
||||
<li>
|
||||
<div
|
||||
className="my-7 rounded bg-slate-500 px-4 py-0.5 text-slate-50"
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
maxWidth: '99.8%',
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
maxWidth: "99.8%",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<GlossarySpan content={entry.content} />
|
||||
</div>
|
||||
<div className="mb-3 flex justify-end">
|
||||
<a href={entry.url} target="_blank" className="flex items-center space-x-1">
|
||||
<a
|
||||
href={entry.url}
|
||||
target="_blank"
|
||||
className="flex items-center space-x-1"
|
||||
>
|
||||
<span>aisafety.info</span>
|
||||
<Image src={logo} alt="aisafety.info logo" width={19} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const Entry = ({entry}: {entry: EntryType}) => {
|
||||
export const Entry = ({ entry }: { entry: EntryType }) => {
|
||||
switch (entry.role) {
|
||||
case 'user':
|
||||
return <User entry={entry} />
|
||||
case 'error':
|
||||
return <Error entry={entry} />
|
||||
case 'assistant':
|
||||
return <Assistant entry={entry} />
|
||||
case 'stampy':
|
||||
return <Stampy entry={entry} />
|
||||
case "user":
|
||||
return <User entry={entry} />;
|
||||
case "error":
|
||||
return <Error entry={entry} />;
|
||||
case "assistant":
|
||||
return <Assistant entry={entry} />;
|
||||
case "stampy":
|
||||
return <Stampy entry={entry} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
import {createContext, useContext} from 'react'
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
type GlossaryItem = {
|
||||
term: string
|
||||
pageid: string
|
||||
contents: string
|
||||
}
|
||||
term: string;
|
||||
pageid: string;
|
||||
contents: string;
|
||||
};
|
||||
|
||||
export type Glossary = Map<string, GlossaryItem>
|
||||
export type Glossary = Map<string, GlossaryItem>;
|
||||
|
||||
export const GlossaryContext = createContext<{g: Glossary; r: RegExp} | null>(null)
|
||||
export const GlossaryContext = createContext<{ g: Glossary; r: RegExp } | null>(
|
||||
null
|
||||
);
|
||||
|
||||
// A component which wraps arbitrary html in a span, and injects glossary terms
|
||||
// into it as hoverable pop-up links. The text is immediately rendered normally,
|
||||
// but after the glossary is loaded (which happens once per page, asynchronously),
|
||||
// the glossary terms are replaced with elements.
|
||||
|
||||
export const GlossarySpan: React.FC<{content: string}> = ({content}) => {
|
||||
const g = useContext(GlossaryContext)
|
||||
export const GlossarySpan: React.FC<{ content: string }> = ({ content }) => {
|
||||
const g = useContext(GlossaryContext);
|
||||
|
||||
// If the glossary hasn't loaded yet, just render the text normally.
|
||||
if (g == null) {
|
||||
return <span dangerouslySetInnerHTML={{__html: content}} />
|
||||
return <span dangerouslySetInnerHTML={{ __html: content }} />;
|
||||
}
|
||||
|
||||
const glossary = g.g
|
||||
const glossaryRegex = g.r
|
||||
const glossary = g.g;
|
||||
const glossaryRegex = g.r;
|
||||
|
||||
// Otherwise, replace glossary terms with links. We can do this in
|
||||
// O(n * sum of term lengths) by finding String.prototype.indexOf of
|
||||
@@ -36,17 +38,17 @@ export const GlossarySpan: React.FC<{content: string}> = ({content}) => {
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: content.replace(glossaryRegex!, (match) => {
|
||||
const item = glossary.get(match.toLowerCase())
|
||||
if (item == undefined) return match
|
||||
const item = glossary.get(match.toLowerCase());
|
||||
if (item == undefined) return match;
|
||||
|
||||
const hover_content = item.contents
|
||||
const pageid = item.pageid
|
||||
const hover_content = item.contents;
|
||||
const pageid = item.pageid;
|
||||
|
||||
if (pageid == undefined || pageid.trim() == '') {
|
||||
if (pageid == undefined || pageid.trim() == "") {
|
||||
return `
|
||||
<div class="glossary-hover" nowrap>${hover_content}</div>
|
||||
<span class="glossary-link">${match}</span>
|
||||
`
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
<div class="glossary-hover" nowrap>${hover_content}</div>
|
||||
@@ -55,10 +57,10 @@ export const GlossarySpan: React.FC<{content: string}> = ({content}) => {
|
||||
class="glossary-link">
|
||||
${match}
|
||||
</a>
|
||||
`
|
||||
`;
|
||||
}
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import logo from '../logo.svg'
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import logo from "../logo.svg";
|
||||
|
||||
const Header: React.FC<{page: 'index' | 'semantic' | 'playground'}> = ({page}) => {
|
||||
const Header: React.FC<{ page: "index" | "semantic" | "playground" }> = ({
|
||||
page,
|
||||
}) => {
|
||||
const sidebar =
|
||||
page === 'index' ? (
|
||||
page === "index" ? (
|
||||
<span className="flex flex-1 flex-col justify-start text-right font-semibold">
|
||||
<Link href="/semantic">Show Sources</Link>
|
||||
</span>
|
||||
@@ -13,7 +15,7 @@ const Header: React.FC<{page: 'index' | 'semantic' | 'playground'}> = ({page}) =
|
||||
<span className="flex flex-1 flex-col justify-start text-right font-semibold">
|
||||
<Link href="/">Go Chat</Link>
|
||||
</span>
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="my-4 flex">
|
||||
@@ -21,7 +23,7 @@ const Header: React.FC<{page: 'index' | 'semantic' | 'playground'}> = ({page}) =
|
||||
<h1 className="my-0 flex-1">AI Safety Chatbot</h1>
|
||||
{sidebar}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Header
|
||||
export default Header;
|
||||
|
||||
+17
-17
@@ -5,28 +5,28 @@
|
||||
// the source file for it to be included in the build
|
||||
|
||||
export const Colours = [
|
||||
'bg-red-100 border-red-300 text-red-800',
|
||||
'bg-amber-100 border-amber-300 text-amber-800',
|
||||
'bg-orange-100 border-orange-300 text-orange-800',
|
||||
'bg-lime-100 border-lime-300 text-lime-800',
|
||||
'bg-green-100 border-green-300 text-green-800',
|
||||
'bg-cyan-100 border-cyan-300 text-cyan-800',
|
||||
'bg-blue-100 border-blue-300 text-blue-800',
|
||||
'bg-violet-100 border-violet-300 text-violet-800',
|
||||
'bg-pink-100 border-pink-300 text-pink-800',
|
||||
]
|
||||
"bg-red-100 border-red-300 text-red-800",
|
||||
"bg-amber-100 border-amber-300 text-amber-800",
|
||||
"bg-orange-100 border-orange-300 text-orange-800",
|
||||
"bg-lime-100 border-lime-300 text-lime-800",
|
||||
"bg-green-100 border-green-300 text-green-800",
|
||||
"bg-cyan-100 border-cyan-300 text-cyan-800",
|
||||
"bg-blue-100 border-blue-300 text-blue-800",
|
||||
"bg-violet-100 border-violet-300 text-violet-800",
|
||||
"bg-pink-100 border-pink-300 text-pink-800",
|
||||
];
|
||||
|
||||
export const A: React.FC<{
|
||||
href: string
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
}> = ({href, className, children}) => {
|
||||
href: string;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}> = ({ href, className, children }) => {
|
||||
// link element that only populates the href field if the contents are there
|
||||
return href && href !== '' ? (
|
||||
return href && href !== "" ? (
|
||||
<a className={className} href={href} target="_blank" rel="noreferrer">
|
||||
{children}
|
||||
</a>
|
||||
) : (
|
||||
<a className={className}>{children}</a>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import React, {ReactNode} from 'react'
|
||||
import Head from 'next/head'
|
||||
import Header from './header'
|
||||
import React, { ReactNode } from "react";
|
||||
import Head from "next/head";
|
||||
import Header from "./header";
|
||||
|
||||
const Page: React.FC<{children: ReactNode; page: 'index' | 'semantic'}> = ({page, children}) => {
|
||||
const Page: React.FC<{ children: ReactNode; page: "index" | "semantic" }> = ({
|
||||
page,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -13,6 +16,6 @@ const Page: React.FC<{children: ReactNode; page: 'index' | 'semantic'}> = ({page
|
||||
{children}
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
);
|
||||
};
|
||||
export default Page;
|
||||
|
||||
@@ -1,100 +1,106 @@
|
||||
import React from 'react'
|
||||
import {useState, useEffect} from 'react'
|
||||
import type {Followup} from '../types'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import dynamic from 'next/dynamic'
|
||||
import React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
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?',
|
||||
"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.',
|
||||
"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 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 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,
|
||||
query_source: 'search' | 'followups',
|
||||
query_source: "search" | "followups",
|
||||
enable: (f_set: Followup[] | ((fs: Followup[]) => Followup[])) => void,
|
||||
controller: AbortController
|
||||
) => void
|
||||
onQuery?: (q: string) => any
|
||||
}> = ({search, onQuery}) => {
|
||||
const initial_query = initialQuestions[Math.floor(Math.random() * initialQuestions.length)] || ''
|
||||
) => void;
|
||||
onQuery?: (q: string) => any;
|
||||
}> = ({ search, onQuery }) => {
|
||||
const initial_query =
|
||||
initialQuestions[Math.floor(Math.random() * initialQuestions.length)] || "";
|
||||
|
||||
const [query, setQuery] = useState(initial_query)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [followups, setFollowups] = useState<Followup[]>([])
|
||||
const [controller, setController] = useState(new AbortController())
|
||||
const [query, setQuery] = useState(initial_query);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [followups, setFollowups] = useState<Followup[]>([]);
|
||||
const [controller, setController] = useState(new AbortController());
|
||||
|
||||
const inputRef = React.useRef<HTMLTextAreaElement>(null)
|
||||
const inputRef = React.useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// because everything is async, I can't just manually set state at the
|
||||
// point we do a search. Instead it needs to be passed into the search
|
||||
// method, for some reason.
|
||||
const enable =
|
||||
(controller: AbortController) => (f_set: Followup[] | ((fs: Followup[]) => Followup[])) => {
|
||||
if (!controller.signal.aborted) setQuery('')
|
||||
(controller: AbortController) =>
|
||||
(f_set: Followup[] | ((fs: Followup[]) => Followup[])) => {
|
||||
if (!controller.signal.aborted) setQuery("");
|
||||
|
||||
setLoading(false)
|
||||
setFollowups(f_set)
|
||||
}
|
||||
setLoading(false);
|
||||
setFollowups(f_set);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// set focus on the input box
|
||||
if (!loading) inputRef.current?.focus()
|
||||
}, [loading])
|
||||
if (!loading) inputRef.current?.focus();
|
||||
}, [loading]);
|
||||
|
||||
// on first mount focus and set cursor to end of input
|
||||
useEffect(() => {
|
||||
if (!inputRef.current) return
|
||||
inputRef.current.focus()
|
||||
inputRef.current.selectionStart = inputRef.current.textLength
|
||||
inputRef.current.selectionEnd = inputRef.current.textLength
|
||||
}, [])
|
||||
if (!inputRef.current) return;
|
||||
inputRef.current.focus();
|
||||
inputRef.current.selectionStart = inputRef.current.textLength;
|
||||
inputRef.current.selectionEnd = inputRef.current.textLength;
|
||||
}, []);
|
||||
|
||||
const runSearch = (query: string, searchtype: 'search' | 'followups') => () => {
|
||||
if (loading || query.trim() === '') return
|
||||
const runSearch =
|
||||
(query: string, searchtype: "search" | "followups") => () => {
|
||||
if (loading || query.trim() === "") return;
|
||||
|
||||
setLoading(true)
|
||||
const controller = new AbortController()
|
||||
setController(controller)
|
||||
search(query, 'search', enable(controller), controller)
|
||||
}
|
||||
const cancelSearch = () => controller.abort()
|
||||
setLoading(true);
|
||||
const controller = new AbortController();
|
||||
setController(controller);
|
||||
search(query, "search", enable(controller), controller);
|
||||
};
|
||||
const cancelSearch = () => controller.abort();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-1 flex flex-col items-end">
|
||||
{' '}
|
||||
{" "}
|
||||
{followups.map((followup, i) => {
|
||||
return (
|
||||
<li key={i}>
|
||||
<button
|
||||
className="my-1 border border-gray-300 px-1"
|
||||
onClick={runSearch(followup.pageid + '\n' + followup.text, 'followups')}
|
||||
onClick={runSearch(
|
||||
followup.pageid + "\n" + followup.text,
|
||||
"followups"
|
||||
)}
|
||||
>
|
||||
<span> {followup.text} </span>
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -104,31 +110,31 @@ const SearchBoxInternal: React.FC<{
|
||||
ref={inputRef}
|
||||
value={query}
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value)
|
||||
onQuery && onQuery(e.target.value)
|
||||
setQuery(e.target.value);
|
||||
onQuery && onQuery(e.target.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// if <esc>, blur the input box
|
||||
if (e.key === 'Escape') e.currentTarget.blur()
|
||||
if (e.key === "Escape") e.currentTarget.blur();
|
||||
// if <enter> without <shift>, submit the form (if it's not empty)
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
runSearch(query, 'search')
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
runSearch(query, "search");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="ml-2"
|
||||
type="button"
|
||||
onClick={loading ? cancelSearch : runSearch(query, 'search')}
|
||||
onClick={loading ? cancelSearch : runSearch(query, "search")}
|
||||
>
|
||||
{loading ? 'Cancel' : 'Search'}
|
||||
{loading ? "Cancel" : "Search"}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const SearchBox = dynamic(() => Promise.resolve(SearchBoxInternal), {
|
||||
ssr: false,
|
||||
})
|
||||
});
|
||||
|
||||
+107
-97
@@ -1,4 +1,4 @@
|
||||
import {API_URL, STAMPY_URL, STAMPY_CONTENT_URL} from '../settings'
|
||||
import { API_URL, STAMPY_URL, STAMPY_CONTENT_URL } from "../settings";
|
||||
import type {
|
||||
Citation,
|
||||
Entry,
|
||||
@@ -9,48 +9,48 @@ import type {
|
||||
CurrentSearch,
|
||||
SearchResult,
|
||||
LLMSettings,
|
||||
} from '../types'
|
||||
import {formatCitations, findCitations} from '../components/citations'
|
||||
} from "../types";
|
||||
import { formatCitations, findCitations } from "../components/citations";
|
||||
|
||||
const MAX_FOLLOWUPS = 4
|
||||
const DATA_HEADER = 'data: '
|
||||
const EVENT_END_HEADER = 'event: close'
|
||||
const MAX_FOLLOWUPS = 4;
|
||||
const DATA_HEADER = "data: ";
|
||||
const EVENT_END_HEADER = "event: close";
|
||||
|
||||
type HistoryEntry = {
|
||||
role: 'error' | 'stampy' | 'assistant' | 'user'
|
||||
content: string
|
||||
}
|
||||
role: "error" | "stampy" | "assistant" | "user";
|
||||
content: string;
|
||||
};
|
||||
|
||||
const ignoreAbort = (error: Error) => {
|
||||
if (error.name !== 'AbortError') {
|
||||
throw error
|
||||
if (error.name !== "AbortError") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export async function* iterateData(res: Response) {
|
||||
const reader = res.body!.getReader()
|
||||
var message = ''
|
||||
const reader = res.body!.getReader();
|
||||
var message = "";
|
||||
|
||||
while (true) {
|
||||
const {done, value} = await reader.read()
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) return
|
||||
if (done) return;
|
||||
|
||||
const chunk = new TextDecoder('utf-8').decode(value)
|
||||
for (const line of chunk.split('\n')) {
|
||||
const chunk = new TextDecoder("utf-8").decode(value);
|
||||
for (const line of chunk.split("\n")) {
|
||||
// Most times, it seems that a single read() call will be one SSE "message",
|
||||
// but I'll do the proper aggregation spec thing in case that's not always true.
|
||||
|
||||
if (line.startsWith(EVENT_END_HEADER)) {
|
||||
return
|
||||
return;
|
||||
} else if (line.startsWith(DATA_HEADER)) {
|
||||
message += line.slice(DATA_HEADER.length)
|
||||
message += line.slice(DATA_HEADER.length);
|
||||
// Fixes #43
|
||||
} else if (line !== '') {
|
||||
message += line
|
||||
} else if (message !== '') {
|
||||
yield JSON.parse(message)
|
||||
message = ''
|
||||
} else if (line !== "") {
|
||||
message += line;
|
||||
} else if (message !== "") {
|
||||
yield JSON.parse(message);
|
||||
message = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,50 +61,50 @@ export const extractAnswer = async (
|
||||
setCurrent: (e: CurrentSearch) => void
|
||||
): Promise<SearchResult> => {
|
||||
var result: AssistantEntry = {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
role: "assistant",
|
||||
content: "",
|
||||
citations: [],
|
||||
citationsMap: new Map(),
|
||||
}
|
||||
var followups: Followup[] = []
|
||||
};
|
||||
var followups: Followup[] = [];
|
||||
for await (var data of iterateData(res)) {
|
||||
switch (data.state) {
|
||||
case 'loading':
|
||||
setCurrent({phase: data.phase, ...result})
|
||||
break
|
||||
case "loading":
|
||||
setCurrent({ phase: data.phase, ...result });
|
||||
break;
|
||||
|
||||
case 'citations':
|
||||
case "citations":
|
||||
result = {
|
||||
...result,
|
||||
citations: data?.citations || result?.citations || [],
|
||||
}
|
||||
setCurrent({phase: data.phase, ...result})
|
||||
break
|
||||
};
|
||||
setCurrent({ phase: data.phase, ...result });
|
||||
break;
|
||||
|
||||
case 'streaming':
|
||||
case "streaming":
|
||||
// incrementally build up the response
|
||||
const content = formatCitations((result?.content || '') + data.content)
|
||||
const content = formatCitations((result?.content || "") + data.content);
|
||||
result = {
|
||||
content,
|
||||
role: 'assistant',
|
||||
role: "assistant",
|
||||
citations: result?.citations || [],
|
||||
citationsMap: findCitations(content, result?.citations || []),
|
||||
}
|
||||
setCurrent({phase: 'streaming', ...result})
|
||||
break
|
||||
};
|
||||
setCurrent({ phase: "streaming", ...result });
|
||||
break;
|
||||
|
||||
case 'followups':
|
||||
case "followups":
|
||||
// add any potential followup questions
|
||||
followups = data.followups.map((value: any) => value as Followup)
|
||||
break
|
||||
case 'done':
|
||||
break
|
||||
case 'error':
|
||||
throw data.error
|
||||
followups = data.followups.map((value: any) => value as Followup);
|
||||
break;
|
||||
case "done":
|
||||
break;
|
||||
case "error":
|
||||
throw data.error;
|
||||
}
|
||||
}
|
||||
return {result, followups}
|
||||
}
|
||||
return { result, followups };
|
||||
};
|
||||
|
||||
const fetchLLM = async (
|
||||
sessionId: string,
|
||||
@@ -113,18 +113,18 @@ const fetchLLM = async (
|
||||
history: HistoryEntry[],
|
||||
controller: AbortController
|
||||
): Promise<Response | void> =>
|
||||
fetch(API_URL + '/chat', {
|
||||
fetch(API_URL + "/chat", {
|
||||
signal: controller.signal,
|
||||
method: 'POST',
|
||||
cache: 'no-cache',
|
||||
method: "POST",
|
||||
cache: "no-cache",
|
||||
keepalive: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'text/event-stream',
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
},
|
||||
|
||||
body: JSON.stringify({sessionId, query, history, settings}),
|
||||
}).catch(ignoreAbort)
|
||||
body: JSON.stringify({ sessionId, query, history, settings }),
|
||||
}).catch(ignoreAbort);
|
||||
|
||||
export const queryLLM = async (
|
||||
query: string,
|
||||
@@ -135,58 +135,59 @@ export const queryLLM = async (
|
||||
controller: AbortController
|
||||
): Promise<SearchResult> => {
|
||||
// do SSE on a POST request.
|
||||
const res = await fetchLLM(sessionId, query, settings, history, controller)
|
||||
const res = await fetchLLM(sessionId, query, settings, history, controller);
|
||||
|
||||
if (!res) {
|
||||
return {result: {role: 'error', content: 'No response from server'}}
|
||||
return { result: { role: "error", content: "No response from server" } };
|
||||
} else if (!res.ok) {
|
||||
return {result: {role: 'error', content: 'POST Error: ' + res.status}}
|
||||
return { result: { role: "error", content: "POST Error: " + res.status } };
|
||||
}
|
||||
|
||||
try {
|
||||
return await extractAnswer(res, setCurrent)
|
||||
return await extractAnswer(res, setCurrent);
|
||||
} catch (e) {
|
||||
if ((e as Error)?.name === 'AbortError') {
|
||||
return {result: {role: 'error', content: 'aborted'}}
|
||||
if ((e as Error)?.name === "AbortError") {
|
||||
return { result: { role: "error", content: "aborted" } };
|
||||
}
|
||||
return {
|
||||
result: {role: 'error', content: e ? e.toString() : 'unknown error'},
|
||||
}
|
||||
result: { role: "error", content: e ? e.toString() : "unknown error" },
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cleanStampyContent = (contents: string) =>
|
||||
contents.replace(
|
||||
/<a(.*?)href="\/\?state=([a-zA-Z0-9]+.*?)"(.*?)<\/a>/g,
|
||||
(_, pre, linkParts, post) => `<a${pre}href="${STAMPY_URL}/?state=${linkParts}"${post}</a>`
|
||||
)
|
||||
(_, pre, linkParts, post) =>
|
||||
`<a${pre}href="${STAMPY_URL}/?state=${linkParts}"${post}</a>`
|
||||
);
|
||||
|
||||
export const getStampyContent = async (
|
||||
questionId: string,
|
||||
controller: AbortController
|
||||
): Promise<SearchResult> => {
|
||||
const res = await fetch(`${STAMPY_CONTENT_URL}/${questionId}`, {
|
||||
method: 'GET',
|
||||
method: "GET",
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
}).catch(ignoreAbort)
|
||||
}).catch(ignoreAbort);
|
||||
|
||||
if (!res) {
|
||||
return {result: {role: 'error', content: 'No response from server'}}
|
||||
return { result: { role: "error", content: "No response from server" } };
|
||||
} else if (!res.ok) {
|
||||
return {result: {role: 'error', content: 'POST Error: ' + res.status}}
|
||||
return { result: { role: "error", content: "POST Error: " + res.status } };
|
||||
}
|
||||
|
||||
const data = (await res.json()).data
|
||||
const data = (await res.json()).data;
|
||||
|
||||
let result = {
|
||||
role: 'stampy',
|
||||
role: "stampy",
|
||||
content: cleanStampyContent(data.text),
|
||||
url: `${STAMPY_URL}/?state=${data.pageid}`,
|
||||
} as StampyMessage
|
||||
} as StampyMessage;
|
||||
|
||||
// re-enable the searchbox, with the question that was just answered
|
||||
// removed from the list of possible followups.
|
||||
@@ -196,45 +197,54 @@ export const getStampyContent = async (
|
||||
pageid: f.pageid!,
|
||||
text: f.title!,
|
||||
score: 0,
|
||||
}))
|
||||
}));
|
||||
|
||||
const fpids = new Set(f_new.map((f: Followup) => f.pageid))
|
||||
const fpids = new Set(f_new.map((f: Followup) => f.pageid));
|
||||
const followups = (f_old: Followup[]): Followup[] => {
|
||||
const f_old_filtered = f_old.filter((f) => f.pageid !== data.pageid && !fpids.has(f.pageid))
|
||||
return [...f_new, ...f_old_filtered].slice(0, MAX_FOLLOWUPS)
|
||||
}
|
||||
const f_old_filtered = f_old.filter(
|
||||
(f) => f.pageid !== data.pageid && !fpids.has(f.pageid)
|
||||
);
|
||||
return [...f_new, ...f_old_filtered].slice(0, MAX_FOLLOWUPS);
|
||||
};
|
||||
|
||||
return {followups, result}
|
||||
}
|
||||
return { followups, result };
|
||||
};
|
||||
|
||||
export const runSearch = async (
|
||||
query: string,
|
||||
query_source: 'search' | 'followups',
|
||||
query_source: "search" | "followups",
|
||||
settings: LLMSettings,
|
||||
entries: Entry[],
|
||||
setCurrent: (c: CurrentSearch) => void,
|
||||
sessionId: string,
|
||||
controller: AbortController
|
||||
): Promise<SearchResult> => {
|
||||
if (query_source === 'search') {
|
||||
if (query_source === "search") {
|
||||
const history = entries
|
||||
.filter((entry) => entry.role !== 'error')
|
||||
.filter((entry) => entry.role !== "error")
|
||||
.map((entry) => ({
|
||||
role: entry.role,
|
||||
content: entry.content.trim(),
|
||||
}))
|
||||
}));
|
||||
|
||||
return await queryLLM(query, settings, history, setCurrent, sessionId, controller)
|
||||
return await queryLLM(
|
||||
query,
|
||||
settings,
|
||||
history,
|
||||
setCurrent,
|
||||
sessionId,
|
||||
controller
|
||||
);
|
||||
} else {
|
||||
// ----------------- HUMAN AUTHORED CONTENT RETRIEVAL ------------------
|
||||
const [questionId] = query.split('\n', 2)
|
||||
const [questionId] = query.split("\n", 2);
|
||||
if (questionId) {
|
||||
return await getStampyContent(questionId, controller)
|
||||
return await getStampyContent(questionId, controller);
|
||||
}
|
||||
const result = {
|
||||
role: 'error',
|
||||
content: 'Could not extract Stampy id from ' + query,
|
||||
}
|
||||
return {result} as SearchResult
|
||||
role: "error",
|
||||
content: "Could not extract Stampy id from " + query,
|
||||
};
|
||||
return { result } as SearchResult;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+75
-73
@@ -1,127 +1,129 @@
|
||||
import {type AppType} from 'next/dist/shared/lib/utils'
|
||||
import {useEffect, useState} from 'react'
|
||||
import { type AppType } from "next/dist/shared/lib/utils";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import '~/styles/globals.css'
|
||||
import "~/styles/globals.css";
|
||||
|
||||
import {Glossary, GlossaryContext} from '../components/glossary'
|
||||
import { Glossary, GlossaryContext } from "../components/glossary";
|
||||
|
||||
const MyApp: AppType = ({Component, pageProps}) => {
|
||||
const [glossary, setGlossary] = useState<{g: Glossary; r: RegExp} | null>(null)
|
||||
const MyApp: AppType = ({ Component, pageProps }) => {
|
||||
const [glossary, setGlossary] = useState<{ g: Glossary; r: RegExp } | null>(
|
||||
null
|
||||
);
|
||||
|
||||
// fetch glossary and compile regex once on load
|
||||
useEffect(() => {
|
||||
if (glossary === null)
|
||||
tempHackFetch('/questions/glossary')
|
||||
tempHackFetch("/questions/glossary")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const glossary: Glossary = new Map(Object.entries(data))
|
||||
const glossary: Glossary = new Map(Object.entries(data));
|
||||
const keys = Array.from(glossary.keys())
|
||||
.sort((a, b) => b.length - a.length) // sort by length descending
|
||||
.map((k) => k.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')) // escape regex chars
|
||||
.map((k) => `\\b${k}\\b`) // add word boundaries
|
||||
.map((k) => k.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")) // escape regex chars
|
||||
.map((k) => `\\b${k}\\b`); // add word boundaries
|
||||
|
||||
const regex = new RegExp(keys.join('|'), 'gim')
|
||||
setGlossary({g: glossary, r: regex})
|
||||
})
|
||||
}, [])
|
||||
const regex = new RegExp(keys.join("|"), "gim");
|
||||
setGlossary({ g: glossary, r: regex });
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<GlossaryContext.Provider value={glossary}>
|
||||
<Component {...pageProps} />
|
||||
</GlossaryContext.Provider>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default MyApp
|
||||
export default MyApp;
|
||||
|
||||
// ------------------- hack until server endpoint is working -------------------
|
||||
|
||||
const GLOSSARY_JSON = {
|
||||
'chain of thought prompting': {
|
||||
term: 'chain of thought prompting',
|
||||
pageid: '8EL7',
|
||||
"chain of thought prompting": {
|
||||
term: "chain of thought prompting",
|
||||
pageid: "8EL7",
|
||||
contents:
|
||||
'<p>Chain-of-thought prompting is a technique which makes a language model generate intermediate reasoning steps in its output.</p>\n',
|
||||
"<p>Chain-of-thought prompting is a technique which makes a language model generate intermediate reasoning steps in its output.</p>\n",
|
||||
},
|
||||
'chain-of-thought': {
|
||||
term: 'chain-of-thought',
|
||||
pageid: '8EL7',
|
||||
"chain-of-thought": {
|
||||
term: "chain-of-thought",
|
||||
pageid: "8EL7",
|
||||
contents:
|
||||
'<p>Chain-of-thought prompting is a technique which makes a language model generate intermediate reasoning steps in its output.</p>\n',
|
||||
"<p>Chain-of-thought prompting is a technique which makes a language model generate intermediate reasoning steps in its output.</p>\n",
|
||||
},
|
||||
"goodhart's law": {
|
||||
term: "goodhart's law",
|
||||
pageid: '8185',
|
||||
pageid: "8185",
|
||||
contents:
|
||||
'<p>Goodhart’s law states that when a measure becomes a target, it ceases to be a good measure.</p>\n',
|
||||
"<p>Goodhart’s law states that when a measure becomes a target, it ceases to be a good measure.</p>\n",
|
||||
},
|
||||
'the big g,': {
|
||||
term: 'the big g,',
|
||||
pageid: '8185',
|
||||
"the big g,": {
|
||||
term: "the big g,",
|
||||
pageid: "8185",
|
||||
contents:
|
||||
'<p>Goodhart’s law states that when a measure becomes a target, it ceases to be a good measure.</p>\n',
|
||||
"<p>Goodhart’s law states that when a measure becomes a target, it ceases to be a good measure.</p>\n",
|
||||
},
|
||||
'terminal goals': {
|
||||
term: 'terminal goals',
|
||||
pageid: '',
|
||||
"terminal goals": {
|
||||
term: "terminal goals",
|
||||
pageid: "",
|
||||
contents:
|
||||
'<p>Goals which are valued as ends in themselves, rather than as instrumental to something else.</p>\n',
|
||||
"<p>Goals which are valued as ends in themselves, rather than as instrumental to something else.</p>\n",
|
||||
},
|
||||
'terminal goal': {
|
||||
term: 'terminal goal',
|
||||
pageid: '',
|
||||
"terminal goal": {
|
||||
term: "terminal goal",
|
||||
pageid: "",
|
||||
contents:
|
||||
'<p>Goals which are valued as ends in themselves, rather than as instrumental to something else.</p>\n',
|
||||
"<p>Goals which are valued as ends in themselves, rather than as instrumental to something else.</p>\n",
|
||||
},
|
||||
'orthogonality thesis': {
|
||||
term: 'orthogonality thesis',
|
||||
pageid: '6568',
|
||||
"orthogonality thesis": {
|
||||
term: "orthogonality thesis",
|
||||
pageid: "6568",
|
||||
contents:
|
||||
'<p>The thesis that any level of intelligence is compatible with any terminal goals. This implies that intelligence alone is not enough to make a system moral.</p>\n',
|
||||
"<p>The thesis that any level of intelligence is compatible with any terminal goals. This implies that intelligence alone is not enough to make a system moral.</p>\n",
|
||||
},
|
||||
'instrumental convergence': {
|
||||
term: 'instrumental convergence',
|
||||
pageid: '897I',
|
||||
"instrumental convergence": {
|
||||
term: "instrumental convergence",
|
||||
pageid: "897I",
|
||||
contents:
|
||||
'<p>Instrumental convergence is the idea that different AI agents, each with distinct terminal goals, will end up adopting many of the same instrumental goals.</p>\n',
|
||||
"<p>Instrumental convergence is the idea that different AI agents, each with distinct terminal goals, will end up adopting many of the same instrumental goals.</p>\n",
|
||||
},
|
||||
'instrumentally convergent goals': {
|
||||
term: 'instrumentally convergent goals',
|
||||
pageid: '897I',
|
||||
"instrumentally convergent goals": {
|
||||
term: "instrumentally convergent goals",
|
||||
pageid: "897I",
|
||||
contents:
|
||||
'<p>Instrumental convergence is the idea that different AI agents, each with distinct terminal goals, will end up adopting many of the same instrumental goals.</p>\n',
|
||||
"<p>Instrumental convergence is the idea that different AI agents, each with distinct terminal goals, will end up adopting many of the same instrumental goals.</p>\n",
|
||||
},
|
||||
llm: {
|
||||
term: 'llm',
|
||||
pageid: '',
|
||||
term: "llm",
|
||||
pageid: "",
|
||||
contents:
|
||||
'<p>A large language model is an AI model which has been trained on a large body of text, in order to produce texts in a human-like way.</p>\n',
|
||||
"<p>A large language model is an AI model which has been trained on a large body of text, in order to produce texts in a human-like way.</p>\n",
|
||||
},
|
||||
'large language model': {
|
||||
term: 'large language model',
|
||||
pageid: '',
|
||||
"large language model": {
|
||||
term: "large language model",
|
||||
pageid: "",
|
||||
contents:
|
||||
'<p>A large language model is an AI model which has been trained on a large body of text, in order to produce texts in a human-like way.</p>\n',
|
||||
"<p>A large language model is an AI model which has been trained on a large body of text, in order to produce texts in a human-like way.</p>\n",
|
||||
},
|
||||
'goal misgeneralization': {
|
||||
term: 'goal misgeneralization',
|
||||
pageid: '',
|
||||
"goal misgeneralization": {
|
||||
term: "goal misgeneralization",
|
||||
pageid: "",
|
||||
contents:
|
||||
'<p>pursuing a different goal during deployment from the one that was pursued during training due to distribution shift</p>\n',
|
||||
"<p>pursuing a different goal during deployment from the one that was pursued during training due to distribution shift</p>\n",
|
||||
},
|
||||
interpretability: {
|
||||
term: 'interpretability',
|
||||
pageid: '8241',
|
||||
term: "interpretability",
|
||||
pageid: "8241",
|
||||
contents:
|
||||
'<p>Interpretability is an area of alignment research that aims to make machine learning systems easier for humans to understand.</p>\n',
|
||||
"<p>Interpretability is an area of alignment research that aims to make machine learning systems easier for humans to understand.</p>\n",
|
||||
},
|
||||
'existential risk': {
|
||||
term: 'existential risk',
|
||||
pageid: '89LL',
|
||||
"existential risk": {
|
||||
term: "existential risk",
|
||||
pageid: "89LL",
|
||||
contents:
|
||||
"<p>risks that threaten the destruction of humanity's long-term potential, including human extinction</p>\n",
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const tempHackFetch = (_url: string) => {
|
||||
return new Promise<Response>((resolve, _reject) => {
|
||||
@@ -129,7 +131,7 @@ const tempHackFetch = (_url: string) => {
|
||||
resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(GLOSSARY_JSON),
|
||||
} as unknown as Response)
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
} as unknown as Response);
|
||||
}, 1000);
|
||||
});
|
||||
};
|
||||
|
||||
+23
-23
@@ -1,46 +1,46 @@
|
||||
import {type NextPage} from 'next'
|
||||
import {useState, useEffect} from 'react'
|
||||
import Link from 'next/link'
|
||||
import { type NextPage } from "next";
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
import {queryLLM, getStampyContent, runSearch} from '../hooks/useSearch'
|
||||
import type {Mode} from '../types'
|
||||
import Page from '../components/page'
|
||||
import Chat from '../components/chat'
|
||||
import {Controls} from '../components/controls'
|
||||
import { queryLLM, getStampyContent, runSearch } from "../hooks/useSearch";
|
||||
import type { Mode } from "../types";
|
||||
import Page from "../components/page";
|
||||
import Chat from "../components/chat";
|
||||
import { Controls } from "../components/controls";
|
||||
|
||||
const MAX_FOLLOWUPS = 4
|
||||
const MAX_FOLLOWUPS = 4;
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const [sessionId, setSessionId] = useState('')
|
||||
const [mode, setMode] = useState<[Mode, boolean]>(['default', false])
|
||||
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])
|
||||
if (mode[1]) localStorage.setItem("chat_mode", mode[0]);
|
||||
}, [mode]);
|
||||
|
||||
// initial load
|
||||
useEffect(() => {
|
||||
const mode = (localStorage.getItem('chat_mode') as Mode) || 'default'
|
||||
setMode([mode, true])
|
||||
setSessionId(crypto.randomUUID())
|
||||
}, [])
|
||||
const mode = (localStorage.getItem("chat_mode") as Mode) || "default";
|
||||
setMode([mode, true]);
|
||||
setSessionId(crypto.randomUUID());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Page page="index">
|
||||
<Controls mode={mode} setMode={setMode} />
|
||||
|
||||
<h2 className="bg-red-100 text-red-800">
|
||||
<b>WARNING</b>: This is a very <b>early prototype</b>.{' '}
|
||||
<b>WARNING</b>: This is a very <b>early prototype</b>.{" "}
|
||||
<Link href="http://bit.ly/stampy-chat-issues" target="_blank">
|
||||
Feedback
|
||||
</Link>{' '}
|
||||
</Link>{" "}
|
||||
welcomed.
|
||||
</h2>
|
||||
|
||||
<Chat sessionId={sessionId} settings={{mode: mode[0]}} />
|
||||
<Chat sessionId={sessionId} settings={{ mode: mode[0] }} />
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Home
|
||||
export default Home;
|
||||
|
||||
+151
-126
@@ -1,108 +1,117 @@
|
||||
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 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 {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'
|
||||
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 MAX_FOLLOWUPS = 4;
|
||||
const DEFAULT_PROMPTS = {
|
||||
source: {
|
||||
prefix:
|
||||
'You are a helpful assistant knowledgeable about AI Alignment and Safety. ' +
|
||||
"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',
|
||||
"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",
|
||||
suffix:
|
||||
'\n\n' +
|
||||
"\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.',
|
||||
"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 ' +
|
||||
"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: '',
|
||||
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',
|
||||
"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',
|
||||
"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': {numTokens: 4095, topKBlocks: 10},
|
||||
'gpt-3.5-turbo-16k': {numTokens: 16385, topKBlocks: 30},
|
||||
'gpt-4': {numTokens: 8192, topKBlocks: 20},
|
||||
"gpt-3.5-turbo": { numTokens: 4095, topKBlocks: 10 },
|
||||
"gpt-3.5-turbo-16k": { numTokens: 16385, topKBlocks: 30 },
|
||||
"gpt-4": { numTokens: 8192, topKBlocks: 20 },
|
||||
/* 'gpt-4-32k': {numTokens: 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
|
||||
numTokens: MODELS['gpt-3.5-turbo'].numTokens,
|
||||
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
|
||||
numTokens: MODELS["gpt-3.5-turbo"].numTokens,
|
||||
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 ENCODERS = ["cl100k_base"];
|
||||
|
||||
const updateIn = (obj: {[key: string]: any}, [head, ...rest]: string[], val: any) => {
|
||||
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
|
||||
obj[head] = val;
|
||||
} else {
|
||||
updateIn(obj[head], rest, val)
|
||||
updateIn(obj[head], rest, val);
|
||||
}
|
||||
return obj
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
type Parseable = string | number | undefined
|
||||
type NumberParser = (v: Parseable) => number
|
||||
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
|
||||
}
|
||||
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) =>
|
||||
(
|
||||
min: Parseable,
|
||||
max: Parseable,
|
||||
parser: NumberParser,
|
||||
updater: (v: any) => any
|
||||
) =>
|
||||
(event: ChangeEvent) => {
|
||||
let num = parser((event.target as HTMLInputElement).value)
|
||||
let num = parser((event.target as HTMLInputElement).value);
|
||||
if (isNaN(num)) {
|
||||
return
|
||||
return;
|
||||
} else if (min !== undefined && num < parser(min)) {
|
||||
num = parser(min)
|
||||
num = parser(min);
|
||||
} else if (max !== undefined && num > parser(max)) {
|
||||
num = parser(max)
|
||||
num = parser(max);
|
||||
}
|
||||
updater(num)
|
||||
}
|
||||
updater(num);
|
||||
};
|
||||
|
||||
const SectionHeader = ({text}: {text: string}) => (
|
||||
const SectionHeader = ({ text }: { text: string }) => (
|
||||
<h4 className="col-span-4 text-lg font-semibold">{text}</h4>
|
||||
)
|
||||
);
|
||||
|
||||
const NumberInput = ({
|
||||
field,
|
||||
@@ -116,7 +125,7 @@ const NumberInput = ({
|
||||
}: InputFields) => (
|
||||
<>
|
||||
<label htmlFor={field} className="col-span-3 inline-block">
|
||||
{label}:{' '}
|
||||
{label}:{" "}
|
||||
</label>
|
||||
<input
|
||||
name={field}
|
||||
@@ -126,7 +135,7 @@ const NumberInput = ({
|
||||
type="number"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
);
|
||||
|
||||
const Slider = ({
|
||||
field,
|
||||
@@ -154,25 +163,26 @@ const Slider = ({
|
||||
step={step}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
);
|
||||
|
||||
type ChatSettingsParams = {
|
||||
settings: LLMSettings
|
||||
updateSettings: (updater: (settings: LLMSettings) => LLMSettings) => void
|
||||
}
|
||||
settings: LLMSettings;
|
||||
updateSettings: (updater: (settings: LLMSettings) => LLMSettings) => void;
|
||||
};
|
||||
|
||||
const ChatSettings = ({settings, updateSettings}: ChatSettingsParams) => {
|
||||
const ChatSettings = ({ settings, updateSettings }: ChatSettingsParams) => {
|
||||
const changeVal = (field: string, value: any) =>
|
||||
updateSettings((prev) => ({...prev, [field]: value}))
|
||||
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)
|
||||
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'}}
|
||||
style={{ height: "fit-content" }}
|
||||
>
|
||||
<SectionHeader text="Models" />
|
||||
<label htmlFor="completions-model" className="col-span-2">
|
||||
@@ -183,20 +193,26 @@ const ChatSettings = ({settings, updateSettings}: ChatSettingsParams) => {
|
||||
className="col-span-2"
|
||||
value={settings.completions}
|
||||
onChange={(event: ChangeEvent) => {
|
||||
const value = (event.target as HTMLInputElement).value
|
||||
const {numTokens, topKBlocks} = MODELS[value as keyof typeof MODELS]
|
||||
const prevNumTokens = MODELS[settings.completions as keyof typeof MODELS].numTokens
|
||||
const prevTopKBlocks = MODELS[settings.completions as keyof typeof MODELS].topKBlocks
|
||||
const value = (event.target as HTMLInputElement).value;
|
||||
const { numTokens, topKBlocks } =
|
||||
MODELS[value as keyof typeof MODELS];
|
||||
const prevNumTokens =
|
||||
MODELS[settings.completions as keyof typeof MODELS].numTokens;
|
||||
const prevTopKBlocks =
|
||||
MODELS[settings.completions as keyof typeof MODELS].topKBlocks;
|
||||
|
||||
if (settings.numTokens === prevNumTokens) {
|
||||
changeVal('numTokens', numTokens)
|
||||
changeVal("numTokens", numTokens);
|
||||
} else {
|
||||
changeVal('numTokens', Math.min(settings.numTokens || 0, numTokens))
|
||||
changeVal(
|
||||
"numTokens",
|
||||
Math.min(settings.numTokens || 0, numTokens)
|
||||
);
|
||||
}
|
||||
if (settings.topKBlocks === prevTopKBlocks) {
|
||||
changeVal('topKBlocks', topKBlocks)
|
||||
changeVal("topKBlocks", topKBlocks);
|
||||
}
|
||||
changeVal('completions', value)
|
||||
changeVal("completions", value);
|
||||
}}
|
||||
>
|
||||
{Object.keys(MODELS).map((name) => (
|
||||
@@ -213,7 +229,7 @@ const ChatSettings = ({settings, updateSettings}: ChatSettingsParams) => {
|
||||
name="encoder"
|
||||
className="col-span-2"
|
||||
value={settings.encoder}
|
||||
onChange={update('encoder')}
|
||||
onChange={update("encoder")}
|
||||
>
|
||||
{ENCODERS.map((name) => (
|
||||
<option value={name} key={name}>
|
||||
@@ -229,7 +245,7 @@ const ChatSettings = ({settings, updateSettings}: ChatSettingsParams) => {
|
||||
label="Tokens"
|
||||
min="1"
|
||||
max={MODELS[settings.completions as keyof typeof MODELS].numTokens}
|
||||
updater={updateNum('numTokens')}
|
||||
updater={updateNum("numTokens")}
|
||||
/>
|
||||
<NumberInput
|
||||
field="tokensBuffer"
|
||||
@@ -237,7 +253,7 @@ const ChatSettings = ({settings, updateSettings}: ChatSettingsParams) => {
|
||||
label="Number of tokens to leave as a buffer when calculating remaining tokens"
|
||||
min="0"
|
||||
max={settings.numTokens}
|
||||
updater={updateNum('tokensBuffer')}
|
||||
updater={updateNum("tokensBuffer")}
|
||||
/>
|
||||
|
||||
<SectionHeader text="Prompt options" />
|
||||
@@ -246,48 +262,57 @@ const ChatSettings = ({settings, updateSettings}: ChatSettingsParams) => {
|
||||
field="topKBlocks"
|
||||
label="Number of blocks to use as citations"
|
||||
min="1"
|
||||
updater={updateNum('topKBlocks')}
|
||||
updater={updateNum("topKBlocks")}
|
||||
/>
|
||||
<NumberInput
|
||||
value={settings.maxHistory}
|
||||
field="maxHistory"
|
||||
label="The max number of previous interactions to use"
|
||||
min="0"
|
||||
updater={updateNum('maxHistory')}
|
||||
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')}
|
||||
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')}
|
||||
updater={updateNum("historyFraction")}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
type ChatPromptParams = {
|
||||
settings: LLMSettings
|
||||
query: string
|
||||
history: Entry[]
|
||||
updateSettings: (updater: (settings: LLMSettings) => LLMSettings) => void
|
||||
}
|
||||
settings: LLMSettings;
|
||||
query: string;
|
||||
history: Entry[];
|
||||
updateSettings: (updater: (settings: LLMSettings) => LLMSettings) => void;
|
||||
};
|
||||
|
||||
const ChatPrompts = ({settings, query, history, updateSettings}: ChatPromptParams) => {
|
||||
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}))
|
||||
}
|
||||
...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">
|
||||
@@ -296,14 +321,14 @@ const ChatPrompts = ({settings, query, history, updateSettings}: ChatPromptParam
|
||||
<TextareaAutosize
|
||||
className="border-gray w-full border px-1"
|
||||
value={settings?.prompts?.source?.prefix}
|
||||
onChange={updatePrompt('source', 'prefix')}
|
||||
onChange={updatePrompt("source", "prefix")}
|
||||
/>
|
||||
<div>(This is where sources will be injected)</div>
|
||||
{history.length > 0 && (
|
||||
<TextareaAutosize
|
||||
className="border-gray w-full border px-1"
|
||||
value={settings?.prompts?.source?.suffix}
|
||||
onChange={updatePrompt('source', 'suffix')}
|
||||
onChange={updatePrompt("source", "suffix")}
|
||||
/>
|
||||
)}
|
||||
</details>
|
||||
@@ -324,48 +349,48 @@ const ChatPrompts = ({settings, query, history, updateSettings}: ChatPromptParam
|
||||
<TextareaAutosize
|
||||
className="border-gray w-full border px-1"
|
||||
value={settings?.prompts?.question}
|
||||
onChange={updatePrompt('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')}
|
||||
value={settings?.prompts?.modes[settings.mode || "default"]}
|
||||
onChange={updatePrompt("modes", settings.mode || "default")}
|
||||
/>
|
||||
</details>
|
||||
<div>Q: {query}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const Playground: NextPage = () => {
|
||||
const [sessionId, setSessionId] = useState('')
|
||||
const [settings, updateSettings] = useState<LLMSettings>(DEFAULT_SETTINGS)
|
||||
const [sessionId, setSessionId] = useState("");
|
||||
const [settings, updateSettings] = useState<LLMSettings>(DEFAULT_SETTINGS);
|
||||
|
||||
const [query, setQuery] = useState<string>('')
|
||||
const [history, setHistory] = useState<Entry[]>([])
|
||||
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]}))
|
||||
localStorage.setItem("chat_mode", mode[0]);
|
||||
updateSettings((settings) => ({ ...settings, mode: mode[0] }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// initial load
|
||||
useEffect(() => {
|
||||
const mode = (localStorage.getItem('chat_mode') as Mode) || 'default'
|
||||
setMode([mode, true])
|
||||
setSessionId(crypto.randomUUID())
|
||||
}, [])
|
||||
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'}}>
|
||||
<main style={{ maxWidth: "none" }}>
|
||||
<Header page="playground" />
|
||||
<Controls mode={[settings.mode || 'default', true]} setMode={setMode} />
|
||||
<Controls mode={[settings.mode || "default", true]} setMode={setMode} />
|
||||
<div className="flex">
|
||||
<ChatPrompts
|
||||
settings={settings}
|
||||
@@ -383,7 +408,7 @@ const Playground: NextPage = () => {
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Playground
|
||||
export default Playground;
|
||||
|
||||
+47
-47
@@ -1,47 +1,47 @@
|
||||
import {type NextPage} from 'next'
|
||||
import React, {useState} from 'react'
|
||||
import {API_URL} from '../settings'
|
||||
import type {Followup} from '../types'
|
||||
import Page from '../components/page'
|
||||
import {SearchBox} from '../components/searchbox'
|
||||
import { type NextPage } from "next";
|
||||
import React, { useState } from "react";
|
||||
import { API_URL } from "../settings";
|
||||
import type { Followup } from "../types";
|
||||
import Page from "../components/page";
|
||||
import { SearchBox } from "../components/searchbox";
|
||||
|
||||
const ignoreAbort = (error: Error) => {
|
||||
if (error.name !== 'AbortError') {
|
||||
throw error
|
||||
if (error.name !== "AbortError") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Semantic: NextPage = () => {
|
||||
const [results, setResults] = useState<SemanticEntry[]>([])
|
||||
const [results, setResults] = useState<SemanticEntry[]>([]);
|
||||
|
||||
const semantic_search = async (
|
||||
query: string,
|
||||
_query_source: 'search' | 'followups',
|
||||
_query_source: "search" | "followups",
|
||||
enable: (f_set: Followup[]) => void,
|
||||
controller: AbortController
|
||||
) => {
|
||||
const res = await fetch(API_URL + '/semantic', {
|
||||
method: 'POST',
|
||||
const res = await fetch(API_URL + "/semantic", {
|
||||
method: "POST",
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
body: JSON.stringify({query: query}),
|
||||
}).catch(ignoreAbort)
|
||||
body: JSON.stringify({ query: query }),
|
||||
}).catch(ignoreAbort);
|
||||
|
||||
if (!res) {
|
||||
enable([])
|
||||
return
|
||||
enable([]);
|
||||
return;
|
||||
} else if (!res.ok) {
|
||||
console.error('load failure: ' + res.status)
|
||||
console.error("load failure: " + res.status);
|
||||
}
|
||||
enable([])
|
||||
enable([]);
|
||||
|
||||
const data = await res.json()
|
||||
const data = await res.json();
|
||||
|
||||
setResults(data)
|
||||
}
|
||||
setResults(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<Page page="semantic">
|
||||
@@ -49,14 +49,14 @@ const Semantic: NextPage = () => {
|
||||
<SearchBox search={semantic_search} />
|
||||
<ul>
|
||||
{results.map((entry, i) => (
|
||||
<li key={'entry' + i}>
|
||||
<li key={"entry" + i}>
|
||||
<ShowSemanticEntry entry={entry} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Round trip test. If this works, our heavier usecase probably will (famous last words)
|
||||
// The one real difference is we'll want to send back a series of results as we get
|
||||
@@ -64,39 +64,39 @@ const Semantic: NextPage = () => {
|
||||
// shouldn't be too much harder.
|
||||
|
||||
type SemanticEntry = {
|
||||
title: string
|
||||
authors: string[]
|
||||
date: string
|
||||
url: string
|
||||
tags: string
|
||||
text: string
|
||||
}
|
||||
title: string;
|
||||
authors: string[];
|
||||
date: string;
|
||||
url: string;
|
||||
tags: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
const ShowSemanticEntry: React.FC<{entry: SemanticEntry}> = ({entry}) => {
|
||||
const ShowSemanticEntry: React.FC<{ entry: SemanticEntry }> = ({ entry }) => {
|
||||
return (
|
||||
<div className="my-3">
|
||||
{/* horizontally split first row, title on left, authors on right */}
|
||||
<div className="flex">
|
||||
<h3 className="flex-1 text-xl">{entry.title}</h3>
|
||||
<p className="my-0 flex-1 text-right">
|
||||
{entry.authors.join(', ')} - {entry.date}
|
||||
{entry.authors.join(", ")} - {entry.date}
|
||||
</p>
|
||||
</div>
|
||||
{entry.text.split('\n').map((paragraph, i) => {
|
||||
const p = paragraph.trim()
|
||||
if (p === '') return <></>
|
||||
if (p === '.....') return <hr key={'b' + i} />
|
||||
{entry.text.split("\n").map((paragraph, i) => {
|
||||
const p = paragraph.trim();
|
||||
if (p === "") return <></>;
|
||||
if (p === ".....") return <hr key={"b" + i} />;
|
||||
return (
|
||||
<p className="text-sm" key={'p' + i}>
|
||||
{' '}
|
||||
{paragraph}{' '}
|
||||
<p className="text-sm" key={"p" + i}>
|
||||
{" "}
|
||||
{paragraph}{" "}
|
||||
</p>
|
||||
)
|
||||
);
|
||||
})}
|
||||
|
||||
<a href={entry.url}>Read more</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Semantic
|
||||
export default Semantic;
|
||||
|
||||
+5
-3
@@ -1,3 +1,5 @@
|
||||
export const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:3001'
|
||||
export const STAMPY_URL = process.env.STAMPY_URL || 'https://aisafety.info'
|
||||
export const STAMPY_CONTENT_URL = process.env.STAMPY_CONTENT_URL || `${API_URL}/human`
|
||||
export const API_URL =
|
||||
process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:3001";
|
||||
export const STAMPY_URL = process.env.STAMPY_URL || "https://aisafety.info";
|
||||
export const STAMPY_CONTENT_URL =
|
||||
process.env.STAMPY_CONTENT_URL || `${API_URL}/human`;
|
||||
|
||||
+44
-44
@@ -1,62 +1,62 @@
|
||||
export type Citation = {
|
||||
title: string
|
||||
authors: string[]
|
||||
date: string
|
||||
url: string
|
||||
index: number
|
||||
}
|
||||
title: string;
|
||||
authors: string[];
|
||||
date: string;
|
||||
url: string;
|
||||
index: number;
|
||||
};
|
||||
|
||||
export type Followup = {
|
||||
text: string
|
||||
pageid: string
|
||||
score: number
|
||||
}
|
||||
text: string;
|
||||
pageid: string;
|
||||
score: number;
|
||||
};
|
||||
|
||||
export type Entry = UserEntry | AssistantEntry | ErrorMessage | StampyMessage
|
||||
export type Entry = UserEntry | AssistantEntry | ErrorMessage | StampyMessage;
|
||||
|
||||
export type UserEntry = {
|
||||
role: 'user'
|
||||
content: string
|
||||
}
|
||||
role: "user";
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type AssistantEntry = {
|
||||
role: 'assistant'
|
||||
content: string
|
||||
citations: Citation[]
|
||||
citationsMap: Map<string, Citation>
|
||||
}
|
||||
role: "assistant";
|
||||
content: string;
|
||||
citations: Citation[];
|
||||
citationsMap: Map<string, Citation>;
|
||||
};
|
||||
|
||||
export type ErrorMessage = {
|
||||
role: 'error'
|
||||
content: string
|
||||
}
|
||||
role: "error";
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type StampyMessage = {
|
||||
role: 'stampy'
|
||||
content: string
|
||||
url: string
|
||||
}
|
||||
role: "stampy";
|
||||
content: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type SearchResult = {
|
||||
followups?: Followup[] | ((f: Followup[]) => Followup[])
|
||||
result: Entry
|
||||
}
|
||||
export type CurrentSearch = (AssistantEntry & {phase?: string}) | undefined
|
||||
followups?: Followup[] | ((f: Followup[]) => Followup[]);
|
||||
result: Entry;
|
||||
};
|
||||
export type CurrentSearch = (AssistantEntry & { phase?: string }) | undefined;
|
||||
|
||||
export type Mode = 'rookie' | 'concise' | 'default'
|
||||
export type Mode = "rookie" | "concise" | "default";
|
||||
|
||||
export type LLMSettings = {
|
||||
prompts?: {
|
||||
[key: string]: any
|
||||
}
|
||||
mode?: Mode
|
||||
completions?: string
|
||||
encoder?: string
|
||||
topKBlocks?: number
|
||||
numTokens?: number
|
||||
tokensBuffer?: number
|
||||
maxHistory?: number
|
||||
historyFraction?: number
|
||||
contextFraction?: number
|
||||
[key: string]: any
|
||||
}
|
||||
[key: string]: any;
|
||||
};
|
||||
mode?: Mode;
|
||||
completions?: string;
|
||||
encoder?: string;
|
||||
topKBlocks?: number;
|
||||
numTokens?: number;
|
||||
tokensBuffer?: number;
|
||||
maxHistory?: number;
|
||||
historyFraction?: number;
|
||||
contextFraction?: number;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user