Skip to content

Commit

Permalink
Merge branch 'main' into embed_assistant
Browse files Browse the repository at this point in the history
  • Loading branch information
mishig25 authored Sep 28, 2024
2 parents 8749982 + d60cfa3 commit b19cc4b
Show file tree
Hide file tree
Showing 20 changed files with 203 additions and 44 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ PARQUET_EXPORT_SECRET=#DEPRECATED, use ADMIN_API_SECRET instead

RATE_LIMIT= # /!\ Legacy definition of messages per minute. Use USAGE_LIMITS.messagesPerMinute instead
MESSAGES_BEFORE_LOGIN=# how many messages a user can send in a conversation before having to login. set to 0 to force login right away
PUBLIC_APP_GUEST_MESSAGE=# a message to the guest user. If not set, a default message will be used

APP_BASE="" # base path of the app, e.g. /chat, left blank as default
PUBLIC_APP_NAME=ChatUI # name used as title throughout the app
Expand Down
1 change: 1 addition & 0 deletions chart/env/prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ envVars:
PUBLIC_APP_COLOR: "yellow"
PUBLIC_APP_DESCRIPTION: "Making the community's best AI chat models available to everyone."
PUBLIC_APP_DISCLAIMER_MESSAGE: "Disclaimer: AI is an area of active research with known problems such as biased generation and misinformation. Do not use this application for high-stakes decisions or advice."
PUBLIC_APP_GUEST_MESSAGE: "You have reached the guest message limit, Sign In with a free Hugging Face account to continue using HuggingChat."
PUBLIC_APP_DATA_SHARING: 0
PUBLIC_APP_DISCLAIMER: 1
PUBLIC_PLAUSIBLE_SCRIPT_URL: "/js/script.js"
Expand Down
5 changes: 3 additions & 2 deletions docs/source/configuration/models/providers/google.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ MODELS=`[
"type": "vertex",
"project": "abc-xyz",
"location": "europe-west3",
"model": "gemini-1.5-pro-preview-0409", // model-name

"extraBody": {
"model_version": "gemini-1.5-pro-002",
},
// Optional
"safetyThreshold": "BLOCK_MEDIUM_AND_ABOVE",
"apiEndpoint": "", // alternative api endpoint url,
Expand Down
4 changes: 1 addition & 3 deletions src/lib/components/LoginModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@
{envPublic.PUBLIC_APP_DESCRIPTION}
</p>
<p class="text-balance rounded-xl border bg-white/80 p-2 text-base text-gray-800">
You have reached the guest message limit, <strong class="font-semibold"
>Sign In with a free Hugging Face account</strong
> to continue using HuggingChat.
{envPublic.PUBLIC_APP_GUEST_MESSAGE}
</p>

<form
Expand Down
16 changes: 15 additions & 1 deletion src/lib/components/ModelCardMetadata.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
import CarbonEarth from "~icons/carbon/earth";
import CarbonArrowUpRight from "~icons/carbon/arrow-up-right";
import BIMeta from "~icons/bi/meta";
import CarbonCode from "~icons/carbon/code";
import type { Model } from "$lib/types/Model";
export let model: Pick<Model, "name" | "datasetName" | "websiteUrl" | "modelUrl" | "datasetUrl">;
export let model: Pick<
Model,
"name" | "datasetName" | "websiteUrl" | "modelUrl" | "datasetUrl" | "hasInferenceAPI"
>;
export let variant: "light" | "dark" = "light";
</script>
Expand Down Expand Up @@ -35,6 +39,16 @@
<div class="max-sm:hidden">&nbsp;page</div></a
>
{/if}
{#if model.hasInferenceAPI}
<a
href={"https://huggingface.co/playground?modelId=" + model.name}
target="_blank"
rel="noreferrer"
class="flex items-center hover:underline"
><CarbonCode class="mr-1.5 shrink-0 text-xs text-gray-400" />
API
</a>
{/if}
{#if model.websiteUrl}
<a
href={model.websiteUrl}
Expand Down
70 changes: 70 additions & 0 deletions src/lib/components/ScrollToPreviousBtn.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<script lang="ts">
import { fade } from "svelte/transition";
import { onDestroy } from "svelte";
import IconChevron from "./icons/IconChevron.svelte";
export let scrollNode: HTMLElement;
export { className as class };
let visible = false;
let className = "";
let observer: ResizeObserver | null = null;
$: if (scrollNode) {
destroy();
if (window.ResizeObserver) {
observer = new ResizeObserver(() => {
updateVisibility();
});
observer.observe(scrollNode);
}
scrollNode.addEventListener("scroll", updateVisibility);
}
function updateVisibility() {
if (!scrollNode) return;
visible =
Math.ceil(scrollNode.scrollTop) + 200 < scrollNode.scrollHeight - scrollNode.clientHeight &&
scrollNode.scrollTop > 200;
}
function scrollToPrevious() {
if (!scrollNode) return;
const messages = scrollNode.querySelectorAll('[id^="message-"]');
const scrollTop = scrollNode.scrollTop;
let previousMessage: Element | null = null;
for (let i = messages.length - 1; i >= 0; i--) {
const messageTop =
messages[i].getBoundingClientRect().top +
scrollTop -
scrollNode.getBoundingClientRect().top;
if (messageTop < scrollTop - 1) {
previousMessage = messages[i];
break;
}
}
if (previousMessage) {
previousMessage.scrollIntoView({ behavior: "smooth", block: "start" });
}
}
function destroy() {
observer?.disconnect();
scrollNode?.removeEventListener("scroll", updateVisibility);
}
onDestroy(destroy);
</script>

{#if visible}
<button
transition:fade={{ duration: 150 }}
on:click={scrollToPrevious}
class="btn absolute flex h-[41px] w-[41px] rounded-full border bg-white shadow-md transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:shadow-gray-950 dark:hover:bg-gray-600 {className}"
>
<IconChevron classNames="rotate-180 mt-[2px]" />
</button>
{/if}
36 changes: 26 additions & 10 deletions src/lib/components/chat/ChatInput.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { isDesktop } from "$lib/utils/isDesktop";
import { browser } from "$app/environment";
import { createEventDispatcher, onMount } from "svelte";
export let value = "";
Expand All @@ -13,25 +13,41 @@
const dispatch = createEventDispatcher<{ submit: void }>();
function isVirtualKeyboard(): boolean {
if (!browser) return false;
// Check for touch capability
if (navigator.maxTouchPoints > 0) return true;
// Check for touch events
if ("ontouchstart" in window) return true;
// Fallback to user agent string check
const userAgent = navigator.userAgent.toLowerCase();
return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent);
}
$: minHeight = `${1 + minRows * 1.5}em`;
$: maxHeight = maxRows ? `${1 + maxRows * 1.5}em` : `auto`;
function handleKeydown(event: KeyboardEvent) {
// submit on enter
if (event.key === "Enter" && !event.shiftKey && !isCompositionOn) {
event.preventDefault();
// blur to close keyboard on mobile
textareaElement.blur();
// refocus so that user on desktop can start typing without needing to reclick on textarea
if (isDesktop(window)) {
textareaElement.focus();
if (isVirtualKeyboard()) {
// Insert a newline at the cursor position
const start = textareaElement.selectionStart;
const end = textareaElement.selectionEnd;
value = value.substring(0, start) + "\n" + value.substring(end);
textareaElement.selectionStart = textareaElement.selectionEnd = start + 1;
} else {
dispatch("submit");
}
dispatch("submit"); // use a custom event instead of `event.target.form.requestSubmit()` as it does not work on Safari 14
}
}
onMount(() => {
if (isDesktop(window)) {
if (!isVirtualKeyboard()) {
textareaElement.focus();
}
});
Expand All @@ -44,7 +60,7 @@
style="min-height: {minHeight}; max-height: {maxHeight}">{(value || " ") + "\n"}</pre>

<textarea
enterkeyhint="send"
enterkeyhint={!isVirtualKeyboard() ? "enter" : "send"}
tabindex="0"
rows="1"
class="scrollbar-custom absolute top-0 m-0 h-full w-full resize-none scroll-p-3 overflow-x-hidden overflow-y-scroll border-0 bg-transparent p-3 outline-none focus:ring-0 focus-visible:ring-0"
Expand Down
2 changes: 2 additions & 0 deletions src/lib/components/chat/ChatMessage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
{#if message.from === "assistant"}
<div
class="group relative -mb-4 flex items-start justify-start gap-4 pb-4 leading-relaxed"
id="message-assistant-{message.id}"
role="presentation"
on:click={() => (isTapped = !isTapped)}
on:keydown={() => (isTapped = !isTapped)}
Expand Down Expand Up @@ -372,6 +373,7 @@
{#if message.from === "user"}
<div
class="group relative w-full items-start justify-start gap-4 max-sm:text-sm"
id="message-user-{message.id}"
role="presentation"
on:click={() => (isTapped = !isTapped)}
on:keydown={() => (isTapped = !isTapped)}
Expand Down
9 changes: 8 additions & 1 deletion src/lib/components/chat/ChatWindow.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import AssistantIntroduction from "./AssistantIntroduction.svelte";
import ChatMessage from "./ChatMessage.svelte";
import ScrollToBottomBtn from "../ScrollToBottomBtn.svelte";
import ScrollToPreviousBtn from "../ScrollToPreviousBtn.svelte";
import { browser } from "$app/environment";
import { snapScrollToBottom } from "$lib/actions/snapScrollToBottom";
import SystemPromptModal from "../SystemPromptModal.svelte";
Expand Down Expand Up @@ -329,8 +330,14 @@
/>
{/if}
</div>

<ScrollToPreviousBtn
class="fixed right-4 max-md:bottom-[calc(50%+26px)] md:bottom-48 lg:right-10"
scrollNode={chatContainer}
/>

<ScrollToBottomBtn
class="bottom-36 right-4 max-md:hidden lg:right-10"
class="fixed right-4 max-md:bottom-[calc(50%-26px)] md:bottom-36 lg:right-10"
scrollNode={chatContainer}
/>
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/lib/server/endpoints/cohere/endpointCohere.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export async function endpointCohere(
});

stream = await cohere.chatStream({
forceSingleStep: true,
message: prompt,
rawPrompting: true,
model: model.id ?? model.name,
Expand All @@ -82,6 +83,7 @@ export async function endpointCohere(

stream = await cohere
.chatStream({
forceSingleStep: true,
model: model.id ?? model.name,
chatHistory: formattedMessages.slice(0, -1),
message: formattedMessages[formattedMessages.length - 1].message,
Expand Down
5 changes: 3 additions & 2 deletions src/lib/server/endpoints/google/endpointVertex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const endpointVertexParametersSchema = z.object({
model: z.any(), // allow optional and validate against emptiness
type: z.literal("vertex"),
location: z.string().default("europe-west1"),
extraBody: z.object({ model_version: z.string() }).optional(),
project: z.string(),
apiEndpoint: z.string().optional(),
safetyThreshold: z
Expand Down Expand Up @@ -49,7 +50,7 @@ export const endpointVertexParametersSchema = z.object({
});

export function endpointVertex(input: z.input<typeof endpointVertexParametersSchema>): Endpoint {
const { project, location, model, apiEndpoint, safetyThreshold, tools, multimodal } =
const { project, location, model, apiEndpoint, safetyThreshold, tools, multimodal, extraBody } =
endpointVertexParametersSchema.parse(input);

const vertex_ai = new VertexAI({
Expand All @@ -64,7 +65,7 @@ export function endpointVertex(input: z.input<typeof endpointVertexParametersSch
const hasFiles = messages.some((message) => message.files && message.files.length > 0);

const generativeModel = vertex_ai.getGenerativeModel({
model: model.id ?? model.name,
model: extraBody?.model_version ?? model.id ?? model.name,
safetySettings: safetyThreshold
? [
{
Expand Down
15 changes: 3 additions & 12 deletions src/lib/server/endpoints/tgi/endpointTgi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export const endpointTgiParametersSchema = z.object({
supportedMimeTypes: ["image/jpeg", "image/webp"],
preferredMimeType: "image/webp",
maxSizeInMB: 5,
maxWidth: 224,
maxHeight: 224,
maxWidth: 378,
maxHeight: 980,
}),
})
.default({}),
Expand Down Expand Up @@ -81,22 +81,13 @@ export function endpointTgi(input: z.input<typeof endpointTgiParametersSchema>):
};
}

const whiteImage = {
mime: "image/png",
image: Buffer.from(
"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAAQABADAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD+/igAoAKACgD/2Q==",
"base64"
),
};

async function prepareMessage(
isMultimodal: boolean,
message: EndpointMessage,
imageProcessor: ImageProcessor
): Promise<EndpointMessage> {
if (!isMultimodal) return message;

const files = await Promise.all(message.files?.map(imageProcessor) ?? [whiteImage]);
const files = await Promise.all(message.files?.map(imageProcessor) ?? []);
const markdowns = files.map(
(file) => `![](data:${file.mime};base64,${file.image.toString("base64")})`
);
Expand Down
41 changes: 34 additions & 7 deletions src/lib/server/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import JSON5 from "json5";
import { getTokenizer } from "$lib/utils/getTokenizer";
import { logger } from "$lib/server/logger";
import { ToolResultStatus, type ToolInput } from "$lib/types/Tool";
import { isHuggingChat } from "$lib/utils/isHuggingChat";

type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;

Expand Down Expand Up @@ -253,10 +254,6 @@ const processModel = async (m: z.infer<typeof modelConfig>) => ({
parameters: { ...m.parameters, stop_sequences: m.parameters?.stop },
});

export type ProcessedModel = Awaited<ReturnType<typeof processModel>> & {
getEndpoint: () => Promise<Endpoint>;
};

const addEndpoint = (m: Awaited<ReturnType<typeof processModel>>) => ({
...m,
getEndpoint: async (): Promise<Endpoint> => {
Expand Down Expand Up @@ -316,10 +313,40 @@ const addEndpoint = (m: Awaited<ReturnType<typeof processModel>>) => ({
},
});

export const models: ProcessedModel[] = await Promise.all(
modelsRaw.map((e) => processModel(e).then(addEndpoint))
const hasInferenceAPI = async (m: Awaited<ReturnType<typeof processModel>>) => {
if (!isHuggingChat) {
return false;
}

const r = await fetch(`https://huggingface.co/api/models/${m.id}`);

if (!r.ok) {
logger.warn(`Failed to check if ${m.id} has inference API: ${r.statusText}`);
return false;
}

const json = await r.json();

if (json.cardData.inference === false) {
return false;
}

return true;
};

export const models = await Promise.all(
modelsRaw.map((e) =>
processModel(e)
.then(addEndpoint)
.then(async (m) => ({
...m,
hasInferenceAPI: await hasInferenceAPI(m),
}))
)
);

export type ProcessedModel = (typeof models)[number];

// super ugly but not sure how to make typescript happier
export const validModelIdSchema = z.enum(models.map((m) => m.id) as [string, ...string[]]);

Expand Down Expand Up @@ -357,5 +384,5 @@ export const smallModel = env.TASK_MODEL

export type BackendModel = Optional<
typeof defaultModel,
"preprompt" | "parameters" | "multimodal" | "unlisted" | "tools"
"preprompt" | "parameters" | "multimodal" | "unlisted" | "tools" | "hasInferenceAPI"
>;
Loading

0 comments on commit b19cc4b

Please sign in to comment.