Skip to content

Commit

Permalink
feat(assistants): use community tools in assistants (huggingface#1421)
Browse files Browse the repository at this point in the history
* wip: give assistant tool access

* add api endpoints

* add/delete tools from assistant settings

* make tools work with assistants

* feat(tools): add more tools indicator throughout UI

* formatting
  • Loading branch information
nsarrazin authored Aug 21, 2024
1 parent 8458e94 commit e70c9f0
Show file tree
Hide file tree
Showing 19 changed files with 419 additions and 46 deletions.
52 changes: 33 additions & 19 deletions src/lib/components/AssistantSettings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
import CarbonUpload from "~icons/carbon/upload";
import CarbonHelpFilled from "~icons/carbon/help";
import CarbonSettingsAdjust from "~icons/carbon/settings-adjust";
import CarbonTools from "~icons/carbon/tools";
import { useSettingsStore } from "$lib/stores/settings";
import { isHuggingChat } from "$lib/utils/isHuggingChat";
import IconInternet from "./icons/IconInternet.svelte";
import TokensCounter from "./TokensCounter.svelte";
import HoverTooltip from "./HoverTooltip.svelte";
import { findCurrentModel } from "$lib/utils/models";
import AssistantToolPicker from "./AssistantToolPicker.svelte";
type ActionData = {
error: boolean;
Expand Down Expand Up @@ -93,7 +95,9 @@
? "domains"
: false;
let tools = assistant?.tools ?? [];
const regex = /{{\s?url=(.+?)\s?}}/g;
$: templateVariables = [...systemPrompt.matchAll(regex)].map((match) => match[1]);
$: selectedModel = models.find((m) => m.id === modelId);
</script>
Expand Down Expand Up @@ -146,6 +150,8 @@
formData.set("ragLinkList", "");
}

formData.set("tools", tools.join(","));

return async ({ result }) => {
loading = false;
await applyAction(result);
Expand Down Expand Up @@ -403,16 +409,27 @@
</div>
<p class="text-xs text-red-500">{getError("inputMessage1", form)}</p>
</label>
{#if $page.data.user?.isEarlyAccess && selectedModel?.tools}
<div>
<span class="text-smd font-semibold"
>Tools
<CarbonTools class="inline text-xs text-purple-600" />
<span class="ml-1 rounded bg-gray-100 px-1 py-0.5 text-xxs font-normal text-gray-600"
>Experimental</span
>
</span>
<p class="text-xs text-gray-500">
Choose up to 3 community tools that will be used with this assistant.
</p>
</div>
<AssistantToolPicker bind:toolIds={tools} />
{/if}
{#if $page.data.enableAssistantsRAG}
<div class="mb-4 flex flex-col flex-nowrap">
<div class="flex flex-col flex-nowrap pb-4">
<span class="mt-2 text-smd font-semibold"
>Internet access
<IconInternet classNames="inline text-sm text-blue-600" />

<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"
Expand Down Expand Up @@ -507,26 +524,13 @@
/>
<p class="text-xs text-red-500">{getError("ragLinkList", form)}</p>
{/if}

<!-- divider -->
<div class="my-3 ml-0 mr-6 w-full border border-gray-200" />

<label class="text-sm has-[:checked]:font-semibold">
<input type="checkbox" name="dynamicPrompt" bind:checked={dynamicPrompt} />
Dynamic Prompt
<p class="mb-2 text-xs font-normal text-gray-500">
Allow the use of template variables {"{{url=https://example.com/path}}"}
to insert dynamic content into your prompt by making GET requests to specified URLs on
each inference.
</p>
</label>
</div>
{/if}
</div>

<div class="relative col-span-1 flex h-full flex-col">
<div class="mb-1 flex justify-between text-sm">
<span class="font-semibold"> Instructions (System Prompt) </span>
<span class="block font-semibold"> Instructions (System Prompt) </span>
{#if dynamicPrompt && templateVariables.length}
<div class="relative">
<button
Expand All @@ -549,6 +553,16 @@
</div>
{/if}
</div>
<label class="pb-2 text-sm has-[:checked]:font-semibold">
<input type="checkbox" name="dynamicPrompt" bind:checked={dynamicPrompt} />
Dynamic Prompt
<p class="mb-2 text-xs font-normal text-gray-500">
Allow the use of template variables {"{{url=https://example.com/path}}"}
to insert dynamic content into your prompt by making GET requests to specified URLs on each
inference.
</p>
</label>

<div class="relative mb-20 flex h-full flex-col gap-2">
<textarea
name="preprompt"
Expand Down
124 changes: 124 additions & 0 deletions src/lib/components/AssistantToolPicker.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<script lang="ts">
import { base } from "$app/paths";
import type { ToolLogoColor, ToolLogoIcon } from "$lib/types/Tool";
import { debounce } from "$lib/utils/debounce";
import { onMount } from "svelte";
import ToolLogo from "./ToolLogo.svelte";
import CarbonClose from "~icons/carbon/close";
interface ToolSuggestion {
_id: string;
displayName: string;
createdByName: string;
color: ToolLogoColor;
icon: ToolLogoIcon;
}
export let toolIds: string[] = [];
let selectedValues: ToolSuggestion[] = [];
onMount(async () => {
selectedValues = await Promise.all(
toolIds.map(async (id) => await fetch(`${base}/api/tools/${id}`).then((res) => res.json()))
);
await fetchSuggestions("");
});
let inputValue = "";
let maxValues = 3;
let suggestions: ToolSuggestion[] = [];
async function fetchSuggestions(query: string) {
suggestions = (await fetch(`${base}/api/tools/search?q=${query}`).then((res) =>
res.json()
)) satisfies ToolSuggestion[];
}
const debouncedFetch = debounce((query: string) => fetchSuggestions(query), 300);
function addValue(value: ToolSuggestion) {
if (selectedValues.length < maxValues && !selectedValues.includes(value)) {
selectedValues = [...selectedValues, value];
toolIds = [...toolIds, value._id];
inputValue = "";
suggestions = [];
}
}
function removeValue(id: ToolSuggestion["_id"]) {
selectedValues = selectedValues.filter((v) => v._id !== id);
toolIds = selectedValues.map((value) => value._id);
}
</script>

{#if selectedValues.length > 0}
<div class="flex flex-wrap items-center justify-center gap-2">
{#each selectedValues as value}
<div
class="flex items-center justify-center space-x-2 rounded border border-gray-300 bg-gray-200 px-2 py-1"
>
<ToolLogo color={value.color} icon={value.icon} size="sm" />
<div class="flex flex-col items-center justify-center py-1">
<a
href={`${base}/tools/${value._id}`}
target="_blank"
class="line-clamp-1 truncate font-semibold text-blue-600 hover:underline"
>{value.displayName}</a
>
{#if value.createdByName}
<p class="text-center text-xs text-gray-500">
Created by
<a class="underline" href="{base}/tools?user={value.createdByName}" target="_blank"
>{value.createdByName}</a
>
</p>
{:else}
<p class="text-center text-xs text-gray-500">Official HuggingChat tool</p>
{/if}
</div>
<button
on:click|stopPropagation|preventDefault={() => removeValue(value._id)}
class="text-lg text-gray-600"
>
<CarbonClose />
</button>
</div>
{/each}
</div>
{/if}

{#if selectedValues.length < maxValues}
<div class="group relative block">
<input
type="text"
bind:value={inputValue}
on:input={(ev) => {
inputValue = ev.currentTarget.value;
debouncedFetch(inputValue);
}}
disabled={selectedValues.length >= maxValues}
class="w-full rounded border border-gray-200 bg-gray-100 px-3 py-2"
class:opacity-50={selectedValues.length >= maxValues}
class:bg-gray-100={selectedValues.length >= maxValues}
placeholder="Type to search tools..."
/>
{#if suggestions.length > 0}
<div
class="invisible absolute z-10 mt-1 w-full rounded border border-gray-300 bg-white shadow-lg group-focus-within:visible"
>
{#each suggestions as suggestion}
<button
on:click|stopPropagation|preventDefault={() => addValue(suggestion)}
class="w-full cursor-pointer px-3 py-2 text-left hover:bg-blue-500 hover:text-white"
>
{suggestion.displayName}
</button>
{/each}
</div>
{/if}
</div>
{/if}
35 changes: 35 additions & 0 deletions src/lib/components/ToolBadge.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script lang="ts">
import ToolLogo from "./ToolLogo.svelte";
import { base } from "$app/paths";
import { browser } from "$app/environment";
export let toolId: string;
</script>

<div
class="relative flex items-center justify-center space-x-2 rounded border border-gray-300 bg-gray-200 px-2 py-1"
>
{#if browser}
{#await fetch(`${base}/api/tools/${toolId}`).then((res) => res.json()) then value}
<ToolLogo color={value.color} icon={value.icon} size="sm" />
<div class="flex flex-col items-center justify-center py-1">
<a
href={`${base}/tools/${value._id}`}
target="_blank"
class="line-clamp-1 truncate font-semibold text-blue-600 hover:underline"
>{value.displayName}</a
>
{#if value.createdByName}
<p class="text-center text-xs text-gray-500">
Created by
<a class="underline" href="{base}/tools?user={value.createdByName}" target="_blank"
>{value.createdByName}</a
>
</p>
{:else}
<p class="text-center text-xs text-gray-500">Official HuggingChat tool</p>
{/if}
</div>
{/await}
{/if}
</div>
4 changes: 3 additions & 1 deletion src/lib/components/ToolLogo.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
export let color: string;
export let icon: string;
export let size: "md" | "lg" = "md";
export let size: "sm" | "md" | "lg" = "md";
$: gradientColor = (() => {
switch (color) {
Expand Down Expand Up @@ -72,6 +72,8 @@
$: sizeClass = (() => {
switch (size) {
case "sm":
return "size-8";
case "md":
return "size-14";
case "lg":
Expand Down
11 changes: 11 additions & 0 deletions src/lib/components/chat/AssistantIntroduction.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import CarbonCheckmark from "~icons/carbon/checkmark";
import CarbonRenew from "~icons/carbon/renew";
import CarbonUserMultiple from "~icons/carbon/user-multiple";
import CarbonTools from "~icons/carbon/tools";
import { share } from "$lib/utils/share";
import { env as envPublic } from "$env/dynamic/public";
Expand All @@ -30,6 +31,7 @@
| "_id"
| "description"
| "userCount"
| "tools"
>;
const dispatch = createEventDispatcher<{ message: string }>();
Expand Down Expand Up @@ -83,6 +85,15 @@
</p>
{/if}

{#if assistant?.tools?.length}
<div
class="flex h-5 w-fit items-center gap-1 rounded-full bg-purple-500/10 pl-1 pr-2 text-xs"
title="This assistant uses the websearch."
>
<CarbonTools class="text-sm text-purple-600" />
Has tools
</div>
{/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"
Expand Down
1 change: 0 additions & 1 deletion src/lib/server/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ async function getChatPromptRender(
];
}

logger.info({ formattedMessages });
if (toolResults?.length) {
// todo: should update the command r+ tokenizer to support system messages at any location
// or use the `rag` mode without the citations
Expand Down
4 changes: 2 additions & 2 deletions src/lib/server/textGeneration/assistant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ export async function processPreprompt(preprompt: string) {

export async function getAssistantById(id?: ObjectId) {
return collections.assistants
.findOne<Pick<Assistant, "rag" | "dynamicPrompt" | "generateSettings">>(
.findOne<Pick<Assistant, "rag" | "dynamicPrompt" | "generateSettings" | "tools">>(
{ _id: id },
{ projection: { rag: 1, dynamicPrompt: 1, generateSettings: 1 } }
{ projection: { rag: 1, dynamicPrompt: 1, generateSettings: 1, tools: 1 } }
)
.then((a) => a ?? undefined);
}
Expand Down
6 changes: 3 additions & 3 deletions src/lib/server/textGeneration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
getAssistantById,
processPreprompt,
} from "./assistant";
import { filterToolsOnPreferences, runTools } from "./tools";
import { getTools, runTools } from "./tools";
import type { WebSearch } from "$lib/types/WebSearch";
import {
type MessageUpdate,
Expand Down Expand Up @@ -77,8 +77,8 @@ async function* textGenerationWithoutTitle(

let toolResults: ToolResult[] = [];

if (model.tools && !conv.assistantId) {
const tools = await filterToolsOnPreferences(toolsPreference, Boolean(assistant));
if (model.tools) {
const tools = await getTools(toolsPreference, ctx.assistant);
const toolCallsRequired = tools.some((tool) => !toolHasName("directly_answer", tool));
if (toolCallsRequired) toolResults = yield* runTools(ctx, tools, preprompt);
}
Expand Down
Loading

0 comments on commit e70c9f0

Please sign in to comment.