mirror of
https://github.com/wassname/stampy-chat.git
synced 2026-09-09 11:36:23 +08:00
proper removal of previous entries
This commit is contained in:
+5
-1
@@ -44,11 +44,15 @@ def semantic():
|
||||
@app.route('/chat', methods=['POST'])
|
||||
@cross_origin()
|
||||
def chat():
|
||||
query = request.json.get('query')
|
||||
query = request.json.get('query', None)
|
||||
session_id = request.json.get('sessionId')
|
||||
history = request.json.get('history', [])
|
||||
settings = request.json.get('settings', {})
|
||||
|
||||
if query is None:
|
||||
query = history[-1].get('content')
|
||||
history = history[:-1]
|
||||
|
||||
def formatter(item):
|
||||
if isinstance(item, Exception):
|
||||
item = {'state': 'error', 'error': str(item)}
|
||||
|
||||
@@ -5,27 +5,25 @@ import type { Citation, AssistantEntry as AssistantType } from "../types";
|
||||
|
||||
export const AssistantEntry: React.FC<{ entry: AssistantType }> = ({
|
||||
entry,
|
||||
}) => {
|
||||
return (
|
||||
<div className="mt-3 mb-8">
|
||||
{entry.content.split("\n").map((paragraph, i) => (
|
||||
<CitationsBlock
|
||||
key={i}
|
||||
text={paragraph}
|
||||
citations={entry.citationsMap}
|
||||
textRenderer={(t) => <GlossarySpan content={t} />}
|
||||
/>
|
||||
))}
|
||||
<ul className="mt-5">
|
||||
{
|
||||
// show citations
|
||||
Array.from(entry.citationsMap.values()).map((citation) => (
|
||||
<li key={citation.index}>
|
||||
<ShowCitation citation={citation} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}) => (
|
||||
<div className="mt-3 mb-8">
|
||||
{entry.content.split("\n").map((paragraph, i) => (
|
||||
<CitationsBlock
|
||||
key={i}
|
||||
text={paragraph}
|
||||
citations={entry.citationsMap}
|
||||
textRenderer={(t) => <GlossarySpan content={t} />}
|
||||
/>
|
||||
))}
|
||||
<ul className="mt-5">
|
||||
{
|
||||
// show citations
|
||||
Array.from(entry.citationsMap.values()).map((citation) => (
|
||||
<li key={citation.index}>
|
||||
<ShowCitation citation={citation} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
+116
-32
@@ -1,5 +1,11 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { queryLLM, getStampyContent, runSearch } from "../hooks/useSearch";
|
||||
import {
|
||||
queryLLM,
|
||||
getStampyContent,
|
||||
EntryRole,
|
||||
HistoryEntry,
|
||||
} from "../hooks/useSearch";
|
||||
import { initialQuestions } from "../settings";
|
||||
|
||||
import type {
|
||||
CurrentSearch,
|
||||
@@ -8,6 +14,7 @@ import type {
|
||||
AssistantEntry as AssistantEntryType,
|
||||
LLMSettings,
|
||||
Followup,
|
||||
SearchResult,
|
||||
} from "../types";
|
||||
import useCitations from "../hooks/useCitations";
|
||||
import { SearchBox } from "../components/searchbox";
|
||||
@@ -41,6 +48,9 @@ function scroll30() {
|
||||
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
|
||||
}
|
||||
|
||||
const randomQuestion = () =>
|
||||
initialQuestions[Math.floor(Math.random() * initialQuestions.length)] || "";
|
||||
|
||||
export const ChatResponse = ({
|
||||
current,
|
||||
defaultElem,
|
||||
@@ -73,6 +83,22 @@ export const ChatResponse = ({
|
||||
}
|
||||
};
|
||||
|
||||
const makeHistory = (query: string, entries: Entry[]): HistoryEntry[] => {
|
||||
const getRole = (entry: Entry): EntryRole => {
|
||||
if (entry.deleted) return "deleted";
|
||||
if (entry.role === "stampy") return "assistant";
|
||||
return entry.role;
|
||||
};
|
||||
|
||||
const history = entries
|
||||
.filter((entry) => entry.role !== "error")
|
||||
.map((entry) => ({
|
||||
role: getRole(entry),
|
||||
content: entry.content.trim(),
|
||||
}));
|
||||
return [...history, { role: "user", content: query }];
|
||||
};
|
||||
|
||||
type ChatParams = {
|
||||
sessionId: string;
|
||||
settings: LLMSettings;
|
||||
@@ -82,7 +108,11 @@ type ChatParams = {
|
||||
|
||||
const Chat = ({ sessionId, settings, onQuery, onNewEntry }: ChatParams) => {
|
||||
const [entries, setEntries] = useState<Entry[]>([]);
|
||||
|
||||
const [query, setQuery] = useState(randomQuestion());
|
||||
const [current, setCurrent] = useState<CurrentSearch>();
|
||||
const [followups, setFollowups] = useState<Followup[]>([]);
|
||||
const [controller, setController] = useState(new AbortController());
|
||||
const { citations, setEntryCitations } = useCitations();
|
||||
|
||||
const updateCurrent = (current: CurrentSearch) => {
|
||||
@@ -94,48 +124,74 @@ const Chat = ({ sessionId, settings, onQuery, onNewEntry }: ChatParams) => {
|
||||
}
|
||||
};
|
||||
|
||||
const addEntry = (entry: Entry) => {
|
||||
const addResult = (query: string, { result, followups }: SearchResult) => {
|
||||
const userEntry = { role: "user", content: query };
|
||||
setEntries((prev) => {
|
||||
const entries = [...prev, entry];
|
||||
const entries = [...prev, userEntry, result] as Entry[];
|
||||
if (onNewEntry) {
|
||||
onNewEntry(entries);
|
||||
}
|
||||
return entries;
|
||||
});
|
||||
setFollowups(followups || []);
|
||||
setQuery("");
|
||||
scroll30();
|
||||
};
|
||||
|
||||
const search = async (
|
||||
query: string,
|
||||
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]!,
|
||||
const withController =
|
||||
(f: any) =>
|
||||
(...args: any) => {
|
||||
const controller = new AbortController();
|
||||
setController(controller);
|
||||
return f(controller, ...args);
|
||||
};
|
||||
|
||||
const { result, followups } = await runSearch(
|
||||
query,
|
||||
query_source,
|
||||
const search = async (controller: AbortController, query: string) => {
|
||||
// clear the query box, append to entries
|
||||
setFollowups([]);
|
||||
|
||||
const history = makeHistory(query, entries);
|
||||
|
||||
const result = await queryLLM(
|
||||
settings,
|
||||
entries,
|
||||
history,
|
||||
updateCurrent,
|
||||
sessionId,
|
||||
controller
|
||||
);
|
||||
if (result.content !== "aborted") {
|
||||
addEntry(userEntry);
|
||||
addEntry(result);
|
||||
enable(followups || []);
|
||||
scroll30();
|
||||
} else {
|
||||
enable([]);
|
||||
|
||||
if (result.result.content !== "aborted") {
|
||||
addResult(query, result);
|
||||
}
|
||||
setCurrent(undefined);
|
||||
};
|
||||
|
||||
const fetchFollowup = async (
|
||||
controller: AbortController,
|
||||
followup: Followup
|
||||
) => {
|
||||
const result = await getStampyContent(followup.pageid, controller);
|
||||
addResult(followup.text, result);
|
||||
};
|
||||
|
||||
const deleteEntry = (i: number) => {
|
||||
const entry = entries[i];
|
||||
if (entry === undefined) {
|
||||
return;
|
||||
} else if (
|
||||
i === entries.length - 1 &&
|
||||
["assistant", "stampy"].includes(entry.role)
|
||||
) {
|
||||
const prev = entries[i - 1];
|
||||
if (prev !== undefined) setQuery(prev.content);
|
||||
setEntries(entries.slice(0, i - 1));
|
||||
setFollowups([]);
|
||||
} else {
|
||||
entry.deleted = true;
|
||||
setEntries([...entries]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ul className="flex-auto">
|
||||
{entries.map(
|
||||
@@ -145,20 +201,27 @@ const Chat = ({ sessionId, settings, onQuery, onNewEntry }: ChatParams) => {
|
||||
<EntryTag entry={entry} />
|
||||
<span
|
||||
className="delete-item absolute right-5 hidden cursor-pointer group-hover:block"
|
||||
onClick={() => {
|
||||
const entry = entries[i];
|
||||
if (entry !== undefined) {
|
||||
entry.deleted = true;
|
||||
setEntries([...entries]);
|
||||
}
|
||||
}}
|
||||
onClick={() => deleteEntry(i)}
|
||||
>
|
||||
✕
|
||||
</span>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
<SearchBox search={search} onQuery={onQuery} />
|
||||
|
||||
<Followups
|
||||
followups={followups}
|
||||
onClick={withController(fetchFollowup)}
|
||||
/>
|
||||
<SearchBox
|
||||
search={withController(search)}
|
||||
query={query}
|
||||
onQuery={(v: string) => {
|
||||
setQuery(v);
|
||||
onQuery && onQuery(v);
|
||||
}}
|
||||
abortSearch={() => controller.abort()}
|
||||
/>
|
||||
<ChatResponse
|
||||
current={current}
|
||||
defaultElem={
|
||||
@@ -170,3 +233,24 @@ const Chat = ({ sessionId, settings, onQuery, onNewEntry }: ChatParams) => {
|
||||
};
|
||||
|
||||
export default Chat;
|
||||
|
||||
const Followups = ({
|
||||
followups,
|
||||
onClick,
|
||||
}: {
|
||||
followups: Followup[];
|
||||
onClick: (f: Followup) => void;
|
||||
}) => (
|
||||
<div className="mt-1 flex flex-col items-end">
|
||||
{followups.map((followup: Followup, i: number) => (
|
||||
<li key={i}>
|
||||
<button
|
||||
className="my-1 border border-gray-300 px-1"
|
||||
onClick={() => onClick(followup)}
|
||||
>
|
||||
<span> {followup.text} </span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,36 +6,15 @@ import TextareaAutosize from "react-textarea-autosize";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const SearchBoxInternal: React.FC<{
|
||||
search: (
|
||||
query: string,
|
||||
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)] || "";
|
||||
|
||||
const [query, setQuery] = useState(initial_query);
|
||||
query: string;
|
||||
search: (query: string) => void;
|
||||
abortSearch: () => void;
|
||||
onQuery: (q: string) => any;
|
||||
}> = ({ query, search, onQuery, abortSearch }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [followups, setFollowups] = useState<Followup[]>([]);
|
||||
const [controller, setController] = useState(new AbortController());
|
||||
|
||||
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("");
|
||||
|
||||
setLoading(false);
|
||||
setFollowups(f_set);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// set focus on the input box
|
||||
if (!loading) inputRef.current?.focus();
|
||||
@@ -49,61 +28,41 @@ const SearchBoxInternal: React.FC<{
|
||||
inputRef.current.selectionEnd = inputRef.current.textLength;
|
||||
}, []);
|
||||
|
||||
const runSearch =
|
||||
(query: string, searchtype: "search" | "followups") => () => {
|
||||
if (loading || query.trim() === "") return;
|
||||
const runSearch = (query: string) => async () => {
|
||||
if (loading || query.trim() === "") return;
|
||||
|
||||
setLoading(true);
|
||||
const controller = new AbortController();
|
||||
setController(controller);
|
||||
search(query, searchtype, enable(controller), controller);
|
||||
};
|
||||
const cancelSearch = () => controller.abort();
|
||||
setLoading(true);
|
||||
await search(query);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const cancelSearch = () => {
|
||||
abortSearch();
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
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"
|
||||
)}
|
||||
>
|
||||
<span> {followup.text} </span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-1 mb-2 flex">
|
||||
<TextareaAutosize
|
||||
className="flex-1 resize-none border border-gray-300 px-1"
|
||||
ref={inputRef}
|
||||
value={query}
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value);
|
||||
onQuery && onQuery(e.target.value);
|
||||
}}
|
||||
onChange={(e) => onQuery(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
// if <esc>, blur the input box
|
||||
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")();
|
||||
runSearch(query)();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="ml-2"
|
||||
type="button"
|
||||
onClick={loading ? cancelSearch : runSearch(query, "search")}
|
||||
onClick={loading ? cancelSearch : runSearch(query)}
|
||||
>
|
||||
{loading ? "Cancel" : "Search"}
|
||||
</button>
|
||||
|
||||
@@ -16,8 +16,8 @@ const MAX_FOLLOWUPS = 4;
|
||||
const DATA_HEADER = "data: ";
|
||||
const EVENT_END_HEADER = "event: close";
|
||||
|
||||
type EntryRole = "error" | "stampy" | "assistant" | "user" | "deleted";
|
||||
type HistoryEntry = {
|
||||
export type EntryRole = "error" | "stampy" | "assistant" | "user" | "deleted";
|
||||
export type HistoryEntry = {
|
||||
role: EntryRole;
|
||||
content: string;
|
||||
};
|
||||
@@ -112,7 +112,6 @@ export const extractAnswer = async (
|
||||
|
||||
const fetchLLM = async (
|
||||
sessionId: string | undefined,
|
||||
query: string,
|
||||
settings: LLMSettings,
|
||||
history: HistoryEntry[],
|
||||
controller: AbortController
|
||||
@@ -127,11 +126,10 @@ const fetchLLM = async (
|
||||
Accept: "text/event-stream",
|
||||
},
|
||||
|
||||
body: JSON.stringify({ sessionId, query, history, settings }),
|
||||
body: JSON.stringify({ sessionId, history, settings }),
|
||||
}).catch(ignoreAbort);
|
||||
|
||||
export const queryLLM = async (
|
||||
query: string,
|
||||
settings: LLMSettings,
|
||||
history: HistoryEntry[],
|
||||
setCurrent: (e?: CurrentSearch) => void,
|
||||
@@ -140,7 +138,7 @@ export const queryLLM = async (
|
||||
): Promise<SearchResult> => {
|
||||
setCurrent({ ...makeEntry(), phase: "started" });
|
||||
// do SSE on a POST request.
|
||||
const res = await fetchLLM(sessionId, query, settings, history, controller);
|
||||
const res = await fetchLLM(sessionId, settings, history, controller);
|
||||
|
||||
if (!res) {
|
||||
return { result: { role: "error", content: "No response from server" } };
|
||||
@@ -214,42 +212,3 @@ export const getStampyContent = async (
|
||||
|
||||
return { followups, result };
|
||||
};
|
||||
|
||||
export const runSearch = async (
|
||||
query: string,
|
||||
query_source: "search" | "followups",
|
||||
settings: LLMSettings,
|
||||
entries: Entry[],
|
||||
setCurrent: (c: CurrentSearch) => void,
|
||||
sessionId: string,
|
||||
controller: AbortController
|
||||
): Promise<SearchResult> => {
|
||||
if (query_source === "search") {
|
||||
const history = entries
|
||||
.filter((entry) => entry.role !== "error")
|
||||
.map((entry) => ({
|
||||
role: (entry.deleted ? "deleted" : entry.role) as EntryRole,
|
||||
content: entry.content.trim(),
|
||||
}));
|
||||
|
||||
return await queryLLM(
|
||||
query,
|
||||
settings,
|
||||
history,
|
||||
setCurrent,
|
||||
sessionId,
|
||||
controller
|
||||
);
|
||||
} else {
|
||||
// ----------------- HUMAN AUTHORED CONTENT RETRIEVAL ------------------
|
||||
const [questionId] = query.split("\n", 2);
|
||||
if (questionId) {
|
||||
return await getStampyContent(questionId, controller);
|
||||
}
|
||||
const result = {
|
||||
role: "error",
|
||||
content: "Could not extract Stampy id from " + query,
|
||||
};
|
||||
return { result } as SearchResult;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,7 @@ import { type NextPage } from "next";
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { queryLLM, getStampyContent, runSearch } from "../hooks/useSearch";
|
||||
import useSettings from "../hooks/useSettings";
|
||||
import type { Mode } from "../types";
|
||||
import Page from "../components/page";
|
||||
import Chat from "../components/chat";
|
||||
import { Controls } from "../components/controls";
|
||||
|
||||
+15
-10
@@ -1,10 +1,13 @@
|
||||
import { type NextPage } from "next";
|
||||
import React, { useState } from "react";
|
||||
import { API_URL } from "../settings";
|
||||
import { API_URL, initialQuestions } from "../settings";
|
||||
import type { Followup } from "../types";
|
||||
import Page from "../components/page";
|
||||
import { SearchBox } from "../components/searchbox";
|
||||
|
||||
const randomQuestion = () =>
|
||||
initialQuestions[Math.floor(Math.random() * initialQuestions.length)] || "";
|
||||
|
||||
const ignoreAbort = (error: Error) => {
|
||||
if (error.name !== "AbortError") {
|
||||
throw error;
|
||||
@@ -12,14 +15,13 @@ const ignoreAbort = (error: Error) => {
|
||||
};
|
||||
|
||||
const Semantic: NextPage = () => {
|
||||
const [query, setQuery] = useState(randomQuestion());
|
||||
const [controller, setController] = useState(new AbortController());
|
||||
const [results, setResults] = useState<SemanticEntry[]>([]);
|
||||
|
||||
const semantic_search = async (
|
||||
query: string,
|
||||
_query_source: "search" | "followups",
|
||||
enable: (f_set: Followup[]) => void,
|
||||
controller: AbortController
|
||||
) => {
|
||||
const semantic_search = async (query: string) => {
|
||||
const controller = new AbortController();
|
||||
setController(controller);
|
||||
const res = await fetch(API_URL + "/semantic", {
|
||||
method: "POST",
|
||||
signal: controller.signal,
|
||||
@@ -31,12 +33,10 @@ const Semantic: NextPage = () => {
|
||||
}).catch(ignoreAbort);
|
||||
|
||||
if (!res) {
|
||||
enable([]);
|
||||
return;
|
||||
} else if (!res.ok) {
|
||||
console.error("load failure: " + res.status);
|
||||
}
|
||||
enable([]);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
@@ -46,7 +46,12 @@ const Semantic: NextPage = () => {
|
||||
return (
|
||||
<Page page="semantic">
|
||||
<h2>Retrieve relevant data sources from alignment research</h2>
|
||||
<SearchBox search={semantic_search} />
|
||||
<SearchBox
|
||||
search={semantic_search}
|
||||
query={query}
|
||||
onQuery={setQuery}
|
||||
abortSearch={() => controller.abort()}
|
||||
/>
|
||||
<ul>
|
||||
{results.map((entry, i) => (
|
||||
<li key={"entry" + i}>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback } from "react";
|
||||
import Page from "../components/page";
|
||||
|
||||
import useCitations from "../hooks/useCitations";
|
||||
import { queryLLM, getStampyContent, runSearch } from "../hooks/useSearch";
|
||||
import { queryLLM, getStampyContent } from "../hooks/useSearch";
|
||||
import useSettings from "../hooks/useSettings";
|
||||
import { initialQuestions } from "../settings";
|
||||
import type {
|
||||
@@ -63,9 +63,8 @@ const Tester: NextPage = () => {
|
||||
selected,
|
||||
index,
|
||||
query: queryLLM(
|
||||
question,
|
||||
settings,
|
||||
[],
|
||||
[{ role: "user", content: question }],
|
||||
updater(index),
|
||||
sessionId,
|
||||
controller
|
||||
|
||||
Reference in New Issue
Block a user