Skip to content

Commit

Permalink
Add websearch controls for assistants (#812)
Browse files Browse the repository at this point in the history
* remove query modifiers from generateQuery

* Add backend for assistant RAG

* Add front-end for updating RAG assistant

* enable web parser to return plaintext directly for matching headers

* Update websearch flow for handling assistant rag preferences

* Add our old blocklist to .env.template

* Enable websearch to run on messages depending on assistant requirements

* reorganized imports

* Rename vars

* use projection

* Add environment variable for assistant rag

* fix assistant rag on runwebsearch

* fix styling if rag is disabled

* make sure we always omit credentials when fetching web pages

* Add new checks for SSRF, with a new env var `ENABLE_LOCAL_FETCH`

* Use DNS to check if the links are local or not

* Add a websearch indicator

* Add more tags to parser

* Add indicators

* Display RAG options in settings view

* ui

* fix rag detection

* bit more spacing

* fix button position in assistant form

* wording (mainly)

* reduce number of tags

* Bump max URLs from 3 to 10

* add ul and ol to parseWeb

* change splitting string

* link style

* wording

* add feedback link

* Update src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte

Co-authored-by: Mishig <[email protected]>

* Update src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte

Co-authored-by: Mishig <[email protected]>

* Update src/routes/assistants/+page.svelte

Co-authored-by: Mishig <[email protected]>

* Update src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte

Co-authored-by: Mishig <[email protected]>

* Update src/lib/components/chat/ChatWindow.svelte

Co-authored-by: Mishig <[email protected]>

* Update src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte

Co-authored-by: Mishig <[email protected]>

* Update src/lib/components/AssistantSettings.svelte

Co-authored-by: Mishig <[email protected]>

* lint

* throw error if not a string

* simplify rag check

---------

Co-authored-by: Mishig <[email protected]>
Co-authored-by: Victor Mustar <[email protected]>
  • Loading branch information
3 people committed Mar 14, 2024
1 parent eb071be commit 3f5871c
Show file tree
Hide file tree
Showing 22 changed files with 469 additions and 84 deletions.
3 changes: 2 additions & 1 deletion .env
Expand Up @@ -135,7 +135,8 @@ EXPOSE_API=true
# PUBLIC_APP_DISCLAIMER=1

ENABLE_ASSISTANTS=false #set to true to enable assistants feature

ENABLE_ASSISTANTS_RAG=false # /!\ This will let users specify arbitrary URLs that the server will then request. Make sure you have the proper firewall rules in place.
ENABLE_LOCAL_FETCH=false #set to true to disable the blocklist for local fetches. Only enable this if you have the proper firewall rules to prevent SSRF attacks and understand the implications.
ALTERNATIVE_REDIRECT_URLS=`[]` #valide alternative redirect URL for OAuth

WEBHOOK_URL_REPORT_ASSISTANT=#provide webhook url to get notified when an assistant gets reported
Expand Down
3 changes: 3 additions & 0 deletions .env.template
Expand Up @@ -277,8 +277,11 @@ PUBLIC_PLAUSIBLE_SCRIPT_URL="/js/script.js"
# XFF_DEPTH=2

ENABLE_ASSISTANTS=true
ENABLE_ASSISTANTS_RAG=true
EXPOSE_API=true

ALTERNATIVE_REDIRECT_URLS=`[
huggingchat://login/callback
]`

WEBSEARCH_BLOCKLIST=`["youtube.com", "twitter.com"]`
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -62,6 +62,7 @@
"handlebars": "^4.7.8",
"highlight.js": "^11.7.0",
"image-size": "^1.0.2",
"ip-address": "^9.0.5",
"jsdom": "^22.0.0",
"json5": "^2.2.3",
"marked": "^4.3.0",
Expand Down
145 changes: 139 additions & 6 deletions src/lib/components/AssistantSettings.svelte
Expand Up @@ -5,11 +5,13 @@
import { onMount } from "svelte";
import { applyAction, enhance } from "$app/forms";
import { page } from "$app/stores";
import { base } from "$app/paths";
import CarbonPen from "~icons/carbon/pen";
import CarbonUpload from "~icons/carbon/upload";
import { useSettingsStore } from "$lib/stores/settings";
import { isHuggingChat } from "$lib/utils/isHuggingChat";
type ActionData = {
error: boolean;
Expand Down Expand Up @@ -71,6 +73,14 @@
let deleteExistingAvatar = false;
let loading = false;
let ragMode: false | "links" | "domains" | "all" = assistant?.rag?.allowAllDomains
? "all"
: assistant?.rag?.allowedLinks?.length ?? 0 > 0
? "links"
: (assistant?.rag?.allowedDomains?.length ?? 0) > 0
? "domains"
: false;
</script>

<form
Expand Down Expand Up @@ -103,6 +113,24 @@
}
}

formData.delete("ragMode");

if (ragMode === false || !$page.data.enableAssistantsRAG) {
formData.set("ragAllowAll", "false");
formData.set("ragLinkList", "");
formData.set("ragDomainList", "");
} else if (ragMode === "all") {
formData.set("ragAllowAll", "true");
formData.set("ragLinkList", "");
formData.set("ragDomainList", "");
} else if (ragMode === "links") {
formData.set("ragAllowAll", "false");
formData.set("ragDomainList", "");
} else if (ragMode === "domains") {
formData.set("ragAllowAll", "false");
formData.set("ragLinkList", "");
}

return async ({ result }) => {
loading = false;
await applyAction(result);
Expand All @@ -126,7 +154,7 @@
{/if}

<div class="grid h-full w-full flex-1 grid-cols-2 gap-6 text-sm max-sm:grid-cols-1">
<div class="flex flex-col gap-4">
<div class="col-span-1 flex flex-col gap-4">
<div>
<div class="mb-1 block pb-2 text-sm font-semibold">Avatar</div>
<input
Expand Down Expand Up @@ -255,21 +283,126 @@
</div>
<p class="text-xs text-red-500">{getError("inputMessage1", form)}</p>
</label>
{#if $page.data.enableAssistantsRAG}
<div class="mb-4 flex flex-col flex-nowrap">
<span class="mt-2 text-smd font-semibold"
>Internet access <span
class="ml-1 rounded bg-gray-100 px-1 py-0.5 text-xxs font-normal text-gray-600"
>Experimental</span
>

{#if isHuggingChat}
<a
href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions/385"
target="_blank"
class="ml-0.5 rounded bg-gray-100 px-1 py-0.5 text-xxs font-normal text-gray-700 underline decoration-gray-400"
>Give feedback</a
>
{/if}
</span>

<label class="mt-1">
<input
checked={!ragMode}
on:change={() => (ragMode = false)}
type="radio"
name="ragMode"
value={false}
/>
<span class="my-2 text-sm" class:font-semibold={!ragMode}> Disabled </span>
{#if !ragMode}
<span class="block text-xs text-gray-500">
Assistant won't look for information from Internet and will be faster to answer.
</span>
{/if}
</label>

<label class="mt-1">
<input
checked={ragMode === "all"}
on:change={() => (ragMode = "all")}
type="radio"
name="ragMode"
value={"all"}
/>
<span class="my-2 text-sm" class:font-semibold={ragMode === "all"}> Enabled </span>
{#if ragMode === "all"}
<span class="block text-xs text-gray-500">
Assistant will do a web search on each user request to find information.
</span>
{/if}
</label>

<label class="mt-1">
<input
checked={ragMode === "domains"}
on:change={() => (ragMode = "domains")}
type="radio"
name="ragMode"
value={false}
/>
<span class="my-2 text-sm" class:font-semibold={ragMode === "domains"}>
Domains search
</span>
</label>
{#if ragMode === "domains"}
<span class="mb-2 text-xs text-gray-500">
Specify domains and URLs that the application can search, separated by commas.
</span>

<input
name="ragDomainList"
class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
placeholder="wikipedia.org,bbc.com"
value={assistant?.rag?.allowedDomains?.join(",") ?? ""}
/>
<p class="text-xs text-red-500">{getError("ragDomainList", form)}</p>
{/if}

<label class="mt-1">
<input
checked={ragMode === "links"}
on:change={() => (ragMode = "links")}
type="radio"
name="ragMode"
value={false}
/>
<span class="my-2 text-sm" class:font-semibold={ragMode === "links"}>
Specific Links
</span>
</label>
{#if ragMode === "links"}
<span class="mb-2 text-xs text-gray-500">
Specify a maximum of 10 direct URLs that the Assistant will access. HTML & Plain Text
only, separated by commas
</span>
<input
name="ragLinkList"
class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
placeholder="https://raw.githubusercontent.com/huggingface/chat-ui/main/README.md"
value={assistant?.rag?.allowedLinks.join(",") ?? ""}
/>
<p class="text-xs text-red-500">{getError("ragLinkList", form)}</p>
{/if}
</div>
{/if}
</div>

<label class="flex flex-col">
<div class="mb-1 text-sm font-semibold">Instructions (system prompt)</div>
<div class="col-span-1 flex h-full flex-col">
<span class="mb-1 text-sm font-semibold"> Instructions (system prompt) </span>
<textarea
name="preprompt"
class="min-h-[8lh] flex-1 rounded-lg border-2 border-gray-200 bg-gray-100 p-2 text-sm"
class="mb-20 min-h-[8lh] flex-1 rounded-lg border-2 border-gray-200 bg-gray-100 p-2 text-sm"
placeholder="You'll act as..."
value={assistant?.preprompt ?? ""}
/>
<p class="text-xs text-red-500">{getError("preprompt", form)}</p>
</label>
</div>
</div>

<div class="mt-6 flex justify-end gap-2">
<div
class="ml-auto mt-6 flex w-fit justify-end gap-2 max-sm:fixed max-sm:bottom-6 max-sm:right-6"
>
<a
href={assistant ? `${base}/settings/assistants/${assistant?._id}` : `${base}/settings`}
class="flex items-center justify-center rounded-full bg-gray-200 px-5 py-2 font-semibold text-gray-600"
Expand Down
36 changes: 31 additions & 5 deletions src/lib/components/chat/AssistantIntroduction.svelte
Expand Up @@ -3,21 +3,34 @@
import IconGear from "~icons/bi/gear-fill";
import { base } from "$app/paths";
import type { Assistant } from "$lib/types/Assistant";
import IconInternet from "../icons/IconInternet.svelte";
export let assistant: Pick<
Assistant,
"avatar" | "name" | "modelId" | "createdByName" | "exampleInputs" | "_id" | "description"
| "avatar"
| "name"
| "rag"
| "modelId"
| "createdByName"
| "exampleInputs"
| "_id"
| "description"
>;
const dispatch = createEventDispatcher<{ message: string }>();
$: hasRag =
assistant?.rag?.allowAllDomains ||
(assistant?.rag?.allowedDomains?.length ?? 0) > 0 ||
(assistant?.rag?.allowedLinks?.length ?? 0) > 0;
</script>

<div class="flex h-full w-full flex-col content-center items-center justify-center pb-52">
<div
class="relative mt-auto rounded-2xl bg-gray-100 text-gray-600 dark:border-gray-800 dark:bg-gray-800/60 dark:text-gray-300"
>
<div
class="flex min-w-[80dvw] items-center gap-4 p-4 pr-1 sm:min-w-[440px] md:p-8 md:pt-10 xl:gap-8"
class="mt-3 flex min-w-[80dvw] items-center gap-4 p-4 pr-1 sm:min-w-[440px] md:p-8 md:pt-10 xl:gap-8"
>
{#if assistant.avatar}
<img
Expand All @@ -39,9 +52,21 @@
<p class="-mb-1">Assistant</p>

<p class="text-xl font-bold sm:text-2xl">{assistant.name}</p>
<p class="line-clamp-6 text-sm text-gray-500 dark:text-gray-400">
{assistant.description}
</p>
{#if assistant.description}
<p class="line-clamp-6 text-sm text-gray-500 dark:text-gray-400">
{assistant.description}
</p>
{/if}

{#if hasRag}
<div
class="flex h-5 w-fit items-center gap-1 rounded-full bg-blue-500/10 pl-1 pr-2 text-xs"
title="This assistant uses the websearch."
>
<IconInternet classNames="text-sm text-blue-600" />
Has internet access
</div>
{/if}

{#if assistant.createdByName}
<p class="pt-2 text-sm text-gray-400 dark:text-gray-500">
Expand All @@ -55,6 +80,7 @@
{/if}
</div>
</div>

<div class="absolute right-3 top-3 md:right-4 md:top-4">
<a
href="{base}/settings/assistants/{assistant._id.toString()}"
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/chat/ChatWindow.svelte
Expand Up @@ -138,7 +138,7 @@
bind:this={chatContainer}
>
<div class="mx-auto flex h-full max-w-3xl flex-col gap-6 px-5 pt-6 sm:gap-8 xl:max-w-4xl">
{#if $page.data?.assistant}
{#if $page.data?.assistant && !!messages.length}
<a
class="mx-auto flex items-center gap-1.5 rounded-full border border-gray-100 bg-gray-50 py-1 pl-1 pr-3 text-sm text-gray-800 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
href="{base}/settings/assistants/{$page.data.assistant._id}"
Expand Down
1 change: 1 addition & 0 deletions src/lib/components/icons/IconInternet.svelte
Expand Up @@ -10,6 +10,7 @@
role="img"
width="1em"
height="1em"
fill="currentColor"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 20 20"
>
Expand Down
31 changes: 31 additions & 0 deletions src/lib/server/isURLLocal.spec.ts
@@ -0,0 +1,31 @@
import { isURLLocal } from "./isURLLocal";
import { describe, expect, it } from "vitest";

describe("isURLLocal", async () => {
it("should return true for localhost", async () => {
expect(await isURLLocal(new URL("http://localhost"))).toBe(true);
});
it("should return true for 127.0.0.1", async () => {
expect(await isURLLocal(new URL("http://127.0.0.1"))).toBe(true);
});
it("should return true for 127.254.254.254", async () => {
expect(await isURLLocal(new URL("http://127.254.254.254"))).toBe(true);
});
it("should return false for huggingface.co", async () => {
expect(await isURLLocal(new URL("https://huggingface.co/"))).toBe(false);
});
it("should return true for 127.0.0.1.nip.io", async () => {
expect(await isURLLocal(new URL("http://127.0.0.1.nip.io"))).toBe(true);
});
it("should fail on ipv6", async () => {
await expect(isURLLocal(new URL("http://[::1]"))).rejects.toThrow();
});
it("should fail on ipv6 --1.sslip.io", async () => {
await expect(isURLLocal(new URL("http://--1.sslip.io"))).rejects.toThrow();
});
it("should fail on invalid domain names", async () => {
await expect(
isURLLocal(new URL("http://34329487239847329874923948732984.com/"))
).rejects.toThrow();
});
});
26 changes: 26 additions & 0 deletions src/lib/server/isURLLocal.ts
@@ -0,0 +1,26 @@
import { Address6, Address4 } from "ip-address";

import dns from "node:dns";

export async function isURLLocal(URL: URL): Promise<boolean> {
const isLocal = new Promise<boolean>((resolve, reject) => {
dns.lookup(URL.hostname, (err, address, family) => {
if (err) {
reject(err);
}
if (family === 4) {
const addr = new Address4(address);
resolve(addr.isInSubnet(new Address4("127.0.0.0/8")));
} else if (family === 6) {
const addr = new Address6(address);
resolve(
addr.isLoopback() || addr.isInSubnet(new Address6("::1/128")) || addr.isLinkLocal()
);
} else {
reject(new Error("Unknown IP family"));
}
});
});

return isLocal;
}

0 comments on commit 3f5871c

Please sign in to comment.