Skip to content

Commit

Permalink
Improve visualization of reference documents with tabs per doc_type. …
Browse files Browse the repository at this point in the history
…UI can now stop a request. Use litellm to interact with llm providers (azureai, openai)
  • Loading branch information
vemonet committed Jan 6, 2025
1 parent ebc6cd6 commit 6ab3469
Show file tree
Hide file tree
Showing 11 changed files with 213 additions and 135 deletions.
4 changes: 3 additions & 1 deletion chat-with-context/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ <h2 class="text-xl font-semibold">
</div>

<div class="flex-grow mx-5">
<!-- api="https://chat.expasy.org/" -->
<!-- api="http://localhost:8000/" -->
<chat-with-context
api="https://chat.expasy.org/"
api="http://localhost:8000/"
api-key="%EXPASY_API_KEY%"
examples="Which resources are available at the SIB?,How can I get the HGNC symbol for the protein P68871?,What are the rat orthologs of the human TP53?,Where is expressed the gene ACE2 in human?,Anatomical entities where the INS zebrafish gene is expressed and its gene GO annotations,List the genes in primates orthologous to genes expressed in the fruit fly eye"
></chat-with-context>
Expand Down
126 changes: 79 additions & 47 deletions chat-with-context/src/chat-with-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import "./style.css";
// https://github.com/solidjs/solid/blob/main/packages/solid-element/README.md
// https://github.com/solidjs/templates/tree/main/ts-tailwindcss

type GenContext = {
type RefenceDocument = {
score: number;
payload: {
doc_type: string;
Expand All @@ -33,7 +33,7 @@ type Message = {
role: "assistant" | "user";
content: Accessor<string>;
setContent: Setter<string>;
sources: GenContext[];
docs: RefenceDocument[];
links: Accessor<Links[]>;
setLinks: Setter<Links[]>;
};
Expand All @@ -52,7 +52,9 @@ customElement("chat-with-context", {api: "", examples: "", apiKey: ""}, props =>
const [messages, setMessages] = createSignal<Message[]>([]);
const [warningMsg, setWarningMsg] = createSignal("");
const [loading, setLoading] = createSignal(false);
const [abortController, setAbortController] = createSignal(new AbortController());
const [feedbackSent, setFeedbackSent] = createSignal(false);
const [selectedTab, setSelectedTab] = createSignal("");

const apiUrl = props.api.endsWith("/") ? props.api : props.api + "/";
if (props.api === "") setWarningMsg("Please provide an API URL for the chat component to work.");
Expand All @@ -65,13 +67,10 @@ customElement("chat-with-context", {api: "", examples: "", apiKey: ""}, props =>
let chatContainerEl!: HTMLDivElement;
let inputTextEl!: HTMLTextAreaElement;

// NOTE: the 2 works but for now we want to always run validation
const streamResponse = false;

const appendMessage = (msgContent: string, role: "assistant" | "user" = "assistant", sources: GenContext[] = []) => {
const appendMessage = (msgContent: string, role: "assistant" | "user" = "assistant", docs: RefenceDocument[] = []) => {
const [content, setContent] = createSignal(msgContent);
const [links, setLinks] = createSignal<Links[]>([]);
const newMsg: Message = {content, setContent, role, sources, links, setLinks};
const newMsg: Message = {content, setContent, role, docs, links, setLinks};
const query = extractSparqlQuery(msgContent);
if (query) newMsg.setLinks([{url: query, ...queryLinkLabels}]);
setMessages(messages => [...messages, newMsg]);
Expand All @@ -88,23 +87,27 @@ customElement("chat-with-context", {api: "", examples: "", apiKey: ""}, props =>
async function submitInput(question: string) {
if (!question.trim()) return;
if (loading()) {
setWarningMsg("⏳ Thinking...");
// setWarningMsg("⏳ Thinking...");
return;
}
inputTextEl.value = "";
setLoading(true);
appendMessage(question, "user");
setTimeout(() => fixInputHeight(), 0);
try {
const startTime = Date.now();
// NOTE: the 2 works but for now we want to always run validation
const streamResponse = true;
const response = await fetch(`${apiUrl}chat`, {
method: "POST",
headers: {
"Content-Type": "application/json",
// 'Authorization': `Bearer ${apiKey}`,
},
signal: abortController().signal,
body: JSON.stringify({
messages: messages().map(({content, role}) => ({content: content(), role})),
model: "gpt-4o",
// model: "azure_ai/mistral-large",
max_tokens: 50,
stream: streamResponse,
api_key: props.apiKey,
Expand All @@ -116,20 +119,20 @@ customElement("chat-with-context", {api: "", examples: "", apiKey: ""}, props =>
const reader = response.body?.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
console.log(reader);
// Iterate stream response
while (true) {
if (reader) {
const {value, done} = await reader.read();
if (done) break;
buffer += decoder.decode(value, {stream: true});
let boundary = buffer.lastIndexOf("\n");
// Add a small artificial delay to make streaming feel more natural
// if (boundary > 10000) await new Promise(resolve => setTimeout(resolve, 10));
console.log(boundary, buffer)
if (boundary !== -1) {
const chunk = buffer.slice(0, boundary);
buffer = buffer.slice(boundary + 1);

const lines = chunk.split("\n").filter(line => line.trim() !== "");
for (const line of lines) {
for (const line of chunk.split("\n").filter(line => line.trim() !== "")) {
if (line === "data: [DONE]") {
return;
}
Expand Down Expand Up @@ -172,7 +175,6 @@ customElement("chat-with-context", {api: "", examples: "", apiKey: ""}, props =>
}
// Extract query once message complete
const query = extractSparqlQuery(lastMsg.content());
console.log(query);
if (query) lastMsg.setLinks([{url: query, ...queryLinkLabels}]);
} else {
// Don't stream, await full response with additional checks done on the server
Expand All @@ -187,9 +189,13 @@ customElement("chat-with-context", {api: "", examples: "", apiKey: ""}, props =>
setWarningMsg("An error occurred. Please try again.");
}
}
console.log(`Request completed in ${(Date.now() - startTime) / 1000} seconds`);
} catch (error) {
console.error("Failed to send message", error);
setWarningMsg("An error occurred when querying the API. Please try again or contact an admin.");
if (error instanceof Error && error.name !== 'AbortError') {
console.error("An error occurred when querying the API", error);
setWarningMsg("An error occurred when querying the API. Please try again or contact an admin.");
}
// setWarningMsg("An error occurred when querying the API. Please try again or contact an admin.");
}
setLoading(false);
setFeedbackSent(false);
Expand Down Expand Up @@ -231,14 +237,15 @@ customElement("chat-with-context", {api: "", examples: "", apiKey: ""}, props =>
innerHTML={DOMPurify.sanitize(marked.parse(msg.content()) as string)}
/>

{/* Add sources references dialog */}
{msg.sources.length > 0 && (
{/* Add reference docs dialog */}
{msg.docs.length > 0 && (
<>
<button
class="my-3 mr-1 px-3 py-1 text-sm bg-gray-300 dark:bg-gray-700 rounded-3xl align-middle"
title="See the documents used to generate the response"
onClick={() => {
(document.getElementById(`source-dialog-${iMsg()}`) as HTMLDialogElement).showModal();
setSelectedTab(msg.docs[0].payload.doc_type);
highlightAll();
}}
>
Expand All @@ -259,31 +266,50 @@ customElement("chat-with-context", {api: "", examples: "", apiKey: ""}, props =>
<i data-feather="x" />
</button>
<article class="prose max-w-full p-3">
<For each={msg.sources}>
{(source, iSource) => (
<>
<p>
<code class="mr-1">
{iSource() + 1} - {Math.round(source.score * 1000) / 1000}
<div class="flex space-x-2 mb-4">
<For each={Array.from(new Set(msg.docs.map(doc => doc.payload.doc_type)))}>
{(docType) =>
<button
class={`px-4 py-2 rounded-lg transition-all ${
selectedTab() === docType
? 'bg-gray-600 text-white shadow-md'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`
}
onClick={() => {
setSelectedTab(docType);
highlightAll();
}}
title="Show only this type of document"
>
{docType}
</button>
}</For>
</div>
<For each={msg.docs.filter(doc => doc.payload.doc_type === selectedTab())}>
{(doc, iDoc) => (
<>
<p>
<code class="mr-1">
{iDoc() + 1} - {Math.round(doc.score * 1000) / 1000}
</code>
{doc.payload.question} (
<a href={doc.payload.endpoint_url} target="_blank">
{doc.payload.endpoint_url}
</a>
)
</p>
{getLangForDocType(doc.payload.doc_type).startsWith("language-") ? (
<pre>
<code class={`${getLangForDocType(doc.payload.doc_type)}`}>
{doc.payload.answer}
</code>
{source.payload.question} (
<a href={source.payload.endpoint_url} target="_blank">
{source.payload.endpoint_url}
</a>
)
</p>
{getLangForDocType(source.payload.doc_type).startsWith("language-") ? (
<pre>
<code class={`${getLangForDocType(source.payload.doc_type)}`}>
{source.payload.answer}
</code>
</pre>
) : (
<p>{source.payload.answer}</p>
)}
</>
)}
</For>
</pre>
) : (
<p>{doc.payload.answer}</p>
)}
</>
)}</For>
</article>
</dialog>
</>
Expand Down Expand Up @@ -352,6 +378,11 @@ customElement("chat-with-context", {api: "", examples: "", apiKey: ""}, props =>
class="p-2 flex"
onSubmit={event => {
event.preventDefault();
// Only abort if it's a click event (not from pressing Enter)
if (event.type === 'submit' && event.submitter && loading()) {
abortController().abort();
setAbortController(new AbortController());
}
submitInput(inputTextEl.value);
}}
>
Expand All @@ -372,18 +403,19 @@ customElement("chat-with-context", {api: "", examples: "", apiKey: ""}, props =>
/>
<button
type="submit"
title={loading() ? "Loading..." : "Send question"}
class="ml-2 px-4 py-2 rounded-3xl text-slate-500 bg-slate-200 dark:text-slate-400 dark:bg-slate-700 "
disabled={loading()}
title={loading() ? "Stop generation" : "Send question"}
class="ml-2 px-4 py-2 rounded-3xl text-slate-500 bg-slate-200 dark:text-slate-400 dark:bg-slate-700"
// disabled={loading()}
>
{loading() ? <i data-feather="loader" class="animate-spin" /> : <i data-feather="send" />}
{loading() ? <i data-feather="square" /> : <i data-feather="send" />}
{/* <i data-feather="loader" class="animate-spin" /> */}
</button>
<button
title="Start a new conversation"
class="ml-2 px-4 py-2 rounded-3xl text-slate-500 bg-slate-200 dark:text-slate-400 dark:bg-slate-700"
onClick={() => setMessages([])}
>
<i data-feather="trash" />
<i data-feather="edit" />
</button>
{/* <div
class="ml-4 tooltip-top"
Expand Down
10 changes: 5 additions & 5 deletions chat-with-context/src/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ export function extractSparqlQuery(markdownContent: string) {

export function getLangForDocType(docType: string) {
switch (docType) {
case "sparql_query":
case "SPARQL endpoints query examples":
return "language-sparql";
case "schemaorg_jsonld":
return "language-json";
case "shex":
// case "General information":
// return "language-json";
case "SPARQL endpoints classes schema":
return "language-turtle";
case "ontology":
case "Ontology":
return "language-turtle";
default:
return "";
Expand Down
24 changes: 16 additions & 8 deletions deploy.sh
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
if [ "$1" = "--build" ]; then
ssh_cmd() {
ssh expasychat "sudo -u podman bash -c 'cd /var/containers/podman/sparql-llm ; $1'"
}

if [ "$1" = "build" ]; then
echo "📦️ Re-building"
ssh expasychat 'sudo -u podman bash -c "cd /var/containers/podman/sparql-llm ; git pull ; podman-compose up --force-recreate --build -d"'
ssh_cmd "git pull ; podman-compose up --force-recreate --build -d"

elif [ "$1" = "--clean" ]; then
elif [ "$1" = "clean" ]; then
echo "🧹 Cleaning up the vector database"
ssh expasychat 'sudo -u podman bash -c "cd /var/containers/podman/sparql-llm ; git pull ; rm -rf data/qdrant ; podman-compose up --force-recreate -d"'
ssh_cmd "git pull ; rm -rf data/qdrant ; podman-compose up --force-recreate -d"

elif [ "$1" = "logs" ]; then
ssh_cmd "podman-compose logs api"

elif [ "$1" = "--logs" ]; then
ssh expasychat 'sudo -u podman bash -c "cd /var/containers/podman/sparql-llm ; podman-compose logs api"'
elif [ "$1" = "index" ]; then
echo "🔎 Indexing the vector database"
ssh_cmd "podman-compose run api python src/sparql_llm/index.py"

elif [ "$1" = "--likes" ]; then
elif [ "$1" = "likes" ]; then
mkdir -p data/prod
scp expasychat:/var/containers/podman/sparql-llm/data/logs/likes.jsonl ./data/prod/
scp expasychat:/var/containers/podman/sparql-llm/data/logs/dislikes.jsonl ./data/prod/
scp expasychat:/var/containers/podman/sparql-llm/data/logs/user_questions.log ./data/prod/

else
ssh expasychat 'sudo -u podman bash -c "cd /var/containers/podman/sparql-llm ; git pull ; podman-compose up --force-recreate -d"'
ssh_cmd "git pull ; podman-compose up --force-recreate -d"
fi
15 changes: 12 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ chat = [
"pydantic-settings",
"openai",
"azure-ai-inference",
"litellm",
"qdrant_client",
"tqdm",
# "fastembed",
Expand All @@ -58,7 +59,6 @@ test = [
"ruff",
"mypy",
"pyright",
"pip-tools",
"SPARQLWrapper",
"ipykernel",
"python-dotenv",
Expand All @@ -78,6 +78,11 @@ gpu = [
"fastembed-gpu",
]

# [dependency-groups]
# dev = [
# "pytest >=7.1.3",
# "pytest-cov >=3.0.0",
# ]

[project.urls]
Homepage = "https://github.com/sib-swiss/sparql-llm"
Expand All @@ -102,6 +107,7 @@ post-install-commands = []

# uv venv
# uv pip install ".[chat,gpu]"
# uv pip compile pyproject.toml -o requirements.txt
# uv run python src/sparql_llm/embed_entities.py
[tool.hatch.envs.default.scripts]
fmt = [
Expand All @@ -122,7 +128,7 @@ cov-check = [
"python -c 'import webbrowser; webbrowser.open(\"http://0.0.0.0:3000\")'",
"python -m http.server 3000 --directory ./htmlcov",
]
compile = "pip-compile -o requirements.txt pyproject.toml"
# compile = "pip-compile -o requirements.txt pyproject.toml"


## TOOLS
Expand Down Expand Up @@ -211,4 +217,7 @@ ignore = [
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["I", "F401"] # module imported but unused
# Tests can use magic values, assertions, and relative imports:
"tests/**/*" = ["PLR2004", "S101", "S105", "TID252"]
"tests/**/*" = ["PLR2004", "S101", "S105", "TID252"]

# [tool.uv.workspace]
# members = ["mcp-sparql"]
Loading

0 comments on commit 6ab3469

Please sign in to comment.