From 011ecfd38d9268a34b49ef0444f9b1084c94346d Mon Sep 17 00:00:00 2001 From: Tim Nunamaker Date: Sun, 31 Mar 2024 11:32:43 -0500 Subject: [PATCH 01/17] feat: improve UI form rendering --- .../components/rjsf/AddButton/AddButton.tsx | 3 +- .../ArrayFieldItemTemplate.tsx | 105 ++++++++---------- .../ArrayFieldTemplate/ArrayFieldTemplate.tsx | 21 ++-- .../BaseInputTemplate/BaseInputTemplate.tsx | 7 +- .../rjsf/CheckboxWidget/CheckboxWidget.tsx | 8 +- .../CheckboxesWidget/CheckboxesWidget.tsx | 2 +- .../rjsf/FieldTemplate/FieldTemplate.tsx | 21 ++-- .../components/rjsf/IconButton/IconButton.tsx | 10 +- .../ObjectFieldTemplate.tsx | 4 +- .../rjsf/RadioWidget/RadioWidget.tsx | 12 +- .../rjsf/SubmitButton/SubmitButton.tsx | 2 +- .../WrapIfAdditionalTemplate.tsx | 16 +-- 12 files changed, 96 insertions(+), 115 deletions(-) diff --git a/selfie-ui/src/app/components/rjsf/AddButton/AddButton.tsx b/selfie-ui/src/app/components/rjsf/AddButton/AddButton.tsx index 2f8883b..e87658e 100644 --- a/selfie-ui/src/app/components/rjsf/AddButton/AddButton.tsx +++ b/selfie-ui/src/app/components/rjsf/AddButton/AddButton.tsx @@ -16,8 +16,7 @@ export default function AddButton< return ( */} +
- + +
+ +
+ Advanced options +
+
+
+ {/**/} + setLimit(Number(e.target.value) || undefined)} + min="1" + /> +
+ +
+ {/**/} + setMinScore(Number(e.target.value) || undefined)} + min="0" + max="1" + step="0.01" + /> +
+ +
+ {/**/} + setRelevanceWeight(e.target.value ? Number(e.target.value) : undefined)} + min="0" + max="1" + step="0.01" + /> +
+ +
+ {/**/} + setRecencyWeight(e.target.value ? Number(e.target.value) : undefined)} + min="0" + max="1" + step="0.01" + /> +
+
+
+ + {!!summary && ( diff --git a/selfie-ui/src/app/components/Settings/Settings.tsx b/selfie-ui/src/app/components/Settings/Settings.tsx new file mode 100644 index 0000000..e61952a --- /dev/null +++ b/selfie-ui/src/app/components/Settings/Settings.tsx @@ -0,0 +1,340 @@ +import React, { useEffect, useState } from "react"; +import TailwindForm from "../../components/rjsf"; +import validator from '@rjsf/validator-ajv8'; +import { apiBaseUrl } from "@/app/config"; + + +const Settings = () => { + const [settings, setSettings] = useState({}); + const [models, setModels] = useState({ data: [] }); + + const schema = { + // title: "Settings", + description: "Configure your application settings.", + type: "object", + required: ["method"], + properties: { + method: { + type: "string", + title: "LLM provider", + enum: ["llama.cpp", "litellm"], + enumNames: ['Local (llama.cpp)', 'Other (litellm)'], + default: "llama.cpp" + }, + // name: { type: "string", title: "Name" }, + // description: { type: "string", title: "Description" }, + // apiKey: { type: "string", title: "API Key" }, + // host: { type: "string", title: "Host", default: "http://localhost" }, + // port: { type: "integer", title: "Port", default: 8000 }, + // share: { type: "boolean", title: "Share", default: false }, + // verbose: { type: "boolean", title: "Verbose", default: false }, + }, + allOf: [ + { + if: { + properties: { + method: { + title: "Local", + description: "Local", + const: "llama.cpp", + label: "ugh" + } + }, + }, + then: { + properties: { + gpu: { + type: "boolean", + title: "GPU mode (check this if you have an Nvidia GPU or ARM Apple CPU for improved performance)", + default: false + }, + model: { + // anyOf: [ + // ...[models.data.length && { + // { + // type: "string", + // title: "Select an already-downloaded model", + // enum: models.data.map(m => m.id), + // default: models.data[0]?.id, + // }, + // }].filter(Boolean), + // { + type: "string", + // title: "Type in a model", + title: "Model", + // }, + // ], + }, + }, + required: ["model"], + }, + }, + { + if: { + properties: { method: { const: "litellm" } }, + }, + then: { + properties: { + model: { + type: "string", + title: "Model", + default: "gpt-3.5-turbo", + }, + api_key: { + type: "string", + title: "API Key", + }, + api_base: { + type: "string", + title: "API Base", + }, + environment_variables: { + type: "object", + title: "Environment variables", + additionalProperties: { + type: "string", + }, + }, + }, + required: ["model"], + }, + }, + ], + }; + + console.log(models) + + const uiSchema = { + method: { + "ui:widget": "radio", + }, + model: { + // "ui:autofocus": true, + // "ui:emptyValue": "", + // "ui:placeholder": "ui:emptyValue causes this field to always be valid despite being required", + // "ui:autocomplete": "family-name", + "ui:enableMarkdownInDescription": true, + // "ui:description": "E.g. `ollama/llama2`. Make sure your model is a valid LiteLLM model." + "ui:description": settings.method === "litellm" ? + "E.g. `ollama/llama2`. Make sure your model is a valid LiteLLM model." : + `The following models were previously downloaded and can be used immediately:\n\n ${models?.data.map(m => `• ${m.id}`).join("\n\n")}`, + }, + }; + + const getSettings = async () => { + const response = await fetch(`${apiBaseUrl}/v1/settings`); + const data = await response.json(); + console.log("Fetched settings:", data); + setSettings(data); + } + + const saveSettings = async (formData: any) => { + console.log("Saving settings...", formData); + + await fetch(`${apiBaseUrl}/v1/settings`, { + method: "PUT", + body: JSON.stringify(formData), + headers: { + "Content-Type": "application/json", + }, + }); + + await getSettings(); + }; + + const onSubmit = async (args: { formData: any }) => { + const { formData } = args + console.log(schema, formData) + + // TODO: This is a hack, find a better way. + // Ensure api_key, api_base, model are sent with empty strings if they are not present + const formDataWithDefaults = formData?.method === 'litellm' ? { ...{ + model: undefined, + api_key: undefined, + api_base: undefined, + }, ...formData } : formData; + + try { + await saveSettings(formDataWithDefaults); + console.log("Settings saved successfully!"); + } catch (error) { + console.error("Error saving settings:", error); + } + }; + + useEffect(() => { + (async () => { + await getSettings(); + })(); + }, []); + + useEffect(() => { + (async () => { + if (settings.method === "litellm") { + const response = await fetch(`${apiBaseUrl}/v1/models`); + const { data } = await response.json(); + setModels({ data: data?.filter((model: any) => model.id.endsWith(".gguf"))}) + } + })(); + }, [settings.method]); + + const deepMerge = (target, source) => { + for (const key in source) { + if (source[key] && typeof source[key] === 'object') { + if (!target[key] || typeof target[key] !== 'object') { + target[key] = {}; + } + deepMerge(target[key], source[key]); + } else { + target[key] = source[key]; + } + } + return target; + }; + + const applyPreset = (preset) => { + setSettings(prevSettings => + deepMerge({...prevSettings}, preset.settings) + ); + } + + const renderPreset = (preset) => { + return ( +
+ +
+ ); + } + + const presets = [ + { + label: 'Local Mistral 7B', + docsLink: 'https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF', + settings: { + method: 'llama.cpp', + model: 'TheBloke/Mistral-7B-Instruct-v0.2-GGUF/mistral-7b-instruct-v0.2.Q4_K_M.gguf', + api_key: '', + api_base: '', + } + }, + { + label: 'Local Mixtral 8x7B', + docsLink: 'https://huggingface.co/TheBloke/Mixtral-8x7B-Instruct-v0.1-GGUF', + settings: { + method: 'llama.cpp', + model: 'TheBloke/Mixtral-8x7B-Instruct-v0.1-GGUF/mixtral-8x7b-instruct-v0.1.Q4_K_M.gguf', + api_key: '', + api_base: '', + } + }, + { + label: 'Ollama', + docsLink: 'https://litellm.vercel.app/docs/providers/ollama', + settings: { + method: 'litellm', + model: 'ollama_chat/mistral', + api_key: '', + api_base: '', + } + }, + { + label: 'OpenAI-Compatible', + docsLink: 'https://litellm.vercel.app/docs/providers/openai_compatible', + settings: { + method: 'litellm', + model: 'openai/mistral', + api_key: 'sk-1234', + api_base: 'http://0.0.0.0:4000', + } + }, + { + label: 'OpenRouter', + docsLink: 'https://litellm.vercel.app/docs/providers/openrouter', + settings: { + method: 'litellm', + model: 'openrouter/google/palm-2-chat-bison', + api_key: '', + api_base: '', + environment_variables: { + OPENROUTER_API_KEY: '', + OR_SITE_URL: '', + OR_APP_NAME: '', + }, + } + }, + { + label: 'OpenAI', + docsLink: 'https://litellm.vercel.app/docs/providers/openai', + settings: { + method: 'litellm', + model: 'gpt-3.5-turbo', + api_key: '', + api_base: '', + environment_variables: { + OPENAI_API_KEY: '', + }, + } + }, + // { + // label: 'Custom OpenAI Proxy', + // docsLink: 'https://litellm.vercel.app/docs/providers/custom_openai_proxy', + // settings: { + // method: 'litellm', + // model: 'command-nightly', + // api_key: 'anything', + // api_base: 'https://openai-proxy.berriai.repl.co', + // environment_variables: { + // OPENAI_API_KEY: 'anything', + // }, + // custom_llm_provider: 'openai', # Need to think about how to support this + // } + // }, + ]; + + return ( + <> +

LLM Presets

+ +

Customize your LLM provider using one of the presets below, or manually configure any llama.cpp or LiteLLM-supported model.

+ +
+ {presets.map(renderPreset)} +
+ +

Your Settings

+ + setSettings(formData)} + validator={validator} + formData={settings} + > +
+ +
+
+ + ); +}; + +export default Settings; diff --git a/selfie-ui/src/app/components/Settings/index.tsx b/selfie-ui/src/app/components/Settings/index.tsx new file mode 100644 index 0000000..8a4c4e8 --- /dev/null +++ b/selfie-ui/src/app/components/Settings/index.tsx @@ -0,0 +1,3 @@ +import Settings from "./Settings"; + +export { Settings } diff --git a/selfie-ui/src/app/components/rjsf/CheckboxWidget/CheckboxWidget.tsx b/selfie-ui/src/app/components/rjsf/CheckboxWidget/CheckboxWidget.tsx index dda8832..eab258a 100644 --- a/selfie-ui/src/app/components/rjsf/CheckboxWidget/CheckboxWidget.tsx +++ b/selfie-ui/src/app/components/rjsf/CheckboxWidget/CheckboxWidget.tsx @@ -67,7 +67,7 @@ export default function CheckboxWidget< registry={registry} /> )} -