From 5b781970c60f9e61b8b1e6d079567922dae0fb14 Mon Sep 17 00:00:00 2001 From: Harald Schilly Date: Fri, 23 Feb 2024 13:33:35 +0100 Subject: [PATCH] ollama: starting with configuration + frontend --- .../frontend/account/other-settings.tsx | 37 +++--- .../account/useLanguageModelSetting.tsx | 31 ++++- .../admin/site-settings/row-entry.tsx | 3 +- src/packages/frontend/chat/message.tsx | 2 +- .../codemirror/extensions/ai-formula.tsx | 29 ++++- src/packages/frontend/customize.tsx | 28 +++-- .../frame-editors/code-editor/actions.ts | 4 +- .../frame-editors/frame-tree/format-error.tsx | 14 ++- .../frame-editors/frame-tree/title-bar.tsx | 2 +- .../latex-editor/errors-and-warnings.tsx | 2 +- .../frame-editors/latex-editor/gutters.tsx | 9 +- .../{chatgpt => llm}/context.tsx | 0 .../{chatgpt => llm}/create-chat.ts | 0 .../{chatgpt => llm}/help-me-fix.tsx | 0 .../{chatgpt => llm}/model-switch.tsx | 29 ++++- .../{chatgpt => llm}/shorten-error.ts | 0 .../title-bar-button-tour.tsx | 0 .../{chatgpt => llm}/title-bar-button.tsx | 0 .../frame-editors/{chatgpt => llm}/types.ts | 0 .../frontend/jupyter/chatgpt/error.tsx | 3 +- .../frontend/jupyter/chatgpt/explain.tsx | 4 +- .../jupyter/insert-cell/ai-cell-generator.tsx | 4 +- .../page/home-page/ai-generate-jupyter.tsx | 6 +- src/packages/frontend/projects/store.ts | 22 +++- src/packages/frontend/sagews/chatgpt.ts | 2 +- src/packages/hub/servers/server-settings.ts | 12 +- src/packages/hub/webapp-configuration.ts | 50 ++++++-- .../llm/{call-chatgpt.ts => call-llm.ts} | 2 +- src/packages/server/llm/client.ts | 50 ++++++-- src/packages/server/llm/index.ts | 2 +- src/packages/util/db-schema/openai.ts | 43 +++++-- .../util/db-schema/site-settings-extras.ts | 107 +++++++++++++++--- src/packages/util/types/llm.ts | 7 ++ 33 files changed, 390 insertions(+), 114 deletions(-) rename src/packages/frontend/frame-editors/{chatgpt => llm}/context.tsx (100%) rename src/packages/frontend/frame-editors/{chatgpt => llm}/create-chat.ts (100%) rename src/packages/frontend/frame-editors/{chatgpt => llm}/help-me-fix.tsx (100%) rename src/packages/frontend/frame-editors/{chatgpt => llm}/model-switch.tsx (80%) rename src/packages/frontend/frame-editors/{chatgpt => llm}/shorten-error.ts (100%) rename src/packages/frontend/frame-editors/{chatgpt => llm}/title-bar-button-tour.tsx (100%) rename src/packages/frontend/frame-editors/{chatgpt => llm}/title-bar-button.tsx (100%) rename src/packages/frontend/frame-editors/{chatgpt => llm}/types.ts (100%) rename src/packages/server/llm/{call-chatgpt.ts => call-llm.ts} (98%) diff --git a/src/packages/frontend/account/other-settings.tsx b/src/packages/frontend/account/other-settings.tsx index f1c04281569..e01d3ddbc92 100644 --- a/src/packages/frontend/account/other-settings.tsx +++ b/src/packages/frontend/account/other-settings.tsx @@ -378,28 +378,21 @@ export class OtherSettings extends Component { render_language_model(): Rendered { const projectsStore = redux.getStore("projects"); - const haveOpenAI = projectsStore.hasLanguageModelEnabled( - undefined, - undefined, - "openai", - ); - const haveGoogle = projectsStore.hasLanguageModelEnabled( - undefined, - undefined, - "google", - ); + const enabled = projectsStore.llmEnabledSummary(); + const ollama = redux.getStore("customize").get("ollama")?.toJS() ?? {}; const defaultModel = getValidLanguageModelName( this.props.other_settings.get(SETTINGS_LANGUAGE_MODEL_KEY), - { openai: haveOpenAI, google: haveGoogle }, + enabled, + Object.keys(ollama), ); const options: { value: string; display: JSX.Element }[] = []; for (const key of USER_SELECTABLE_LANGUAGE_MODELS) { const vendor = model2vendor(key); - if (vendor === "google" && !haveGoogle) continue; - if (vendor === "openai" && !haveOpenAI) continue; + if (vendor === "google" && !enabled.google) continue; + if (vendor === "openai" && !enabled.openai) continue; const txt = isFreeModel(key) ? " (free)" : ""; const display = ( @@ -410,6 +403,18 @@ export class OtherSettings extends Component { options.push({ value: key, display }); } + if (enabled.ollama) { + for (const key in ollama) { + const title = ollama[key].display ?? key; + const display = ( + <> + {title} (Ollama) + + ); + options.push({ value: key, display }); + } + } + return ( { redux.getStore("projects").clearOpenAICache(); }} > - Disable all AI integrations, e.g., - code generation or explanation buttons in Jupyter, @chatgpt - mentions, etc. + Disable all AI integrations, e.g., code + generation or explanation buttons in Jupyter, @chatgpt mentions, + etc. )} {this.render_language_model()} diff --git a/src/packages/frontend/account/useLanguageModelSetting.tsx b/src/packages/frontend/account/useLanguageModelSetting.tsx index 3c79ff3ef50..d6719d3627d 100644 --- a/src/packages/frontend/account/useLanguageModelSetting.tsx +++ b/src/packages/frontend/account/useLanguageModelSetting.tsx @@ -2,26 +2,49 @@ import { redux, useMemo, useTypedRedux } from "@cocalc/frontend/app-framework"; import { LanguageModel, USER_SELECTABLE_LANGUAGE_MODELS, + fromOllamaModel, getValidLanguageModelName, + isOllamaLLM, } from "@cocalc/util/db-schema/openai"; export const SETTINGS_LANGUAGE_MODEL_KEY = "language_model"; export function useLanguageModelSetting(): [ - LanguageModel, - (llm: LanguageModel) => void, + LanguageModel | string, + (llm: LanguageModel | string) => void, ] { const other_settings = useTypedRedux("account", "other_settings"); + const ollama = useTypedRedux("customize", "ollama"); + const haveOpenAI = useTypedRedux("customize", "openai_enabled"); + const haveGoogle = useTypedRedux("customize", "google_vertexai_enabled"); + const haveOllama = useTypedRedux("customize", "ollama_enabled"); + + const filter = useMemo(() => { + const projectsStore = redux.getStore("projects"); + return projectsStore.llmEnabledSummary(); + }, [haveOpenAI, haveGoogle, haveOllama]); + const llm = useMemo(() => { - return getValidLanguageModelName(other_settings?.get("language_model")); + return getValidLanguageModelName( + other_settings?.get("language_model"), + filter, + Object.keys(ollama?.toJS() ?? {}), + ); }, [other_settings]); - function setLLM(llm: LanguageModel) { + function setLLM(llm: LanguageModel | string) { if (USER_SELECTABLE_LANGUAGE_MODELS.includes(llm as any)) { redux .getActions("account") .set_other_settings(SETTINGS_LANGUAGE_MODEL_KEY, llm); } + + // check if llm is a key in the ollama typedmap + if (isOllamaLLM(llm) && ollama?.get(fromOllamaModel(llm))) { + redux + .getActions("account") + .set_other_settings(SETTINGS_LANGUAGE_MODEL_KEY, llm); + } } return [llm, setLLM]; diff --git a/src/packages/frontend/admin/site-settings/row-entry.tsx b/src/packages/frontend/admin/site-settings/row-entry.tsx index aa4f3864fc5..1597623bf07 100644 --- a/src/packages/frontend/admin/site-settings/row-entry.tsx +++ b/src/packages/frontend/admin/site-settings/row-entry.tsx @@ -120,7 +120,8 @@ export function RowEntry({ {displayed_val != null && ( {" "} - Interpreted as {displayed_val}.{" "} + {valid ? "Interpreted as" : "Invalid:"}{" "} + {displayed_val}.{" "} )} {valid != null && Array.isArray(valid) && ( diff --git a/src/packages/frontend/chat/message.tsx b/src/packages/frontend/chat/message.tsx index f60a3a75f78..6c51d776afe 100644 --- a/src/packages/frontend/chat/message.tsx +++ b/src/packages/frontend/chat/message.tsx @@ -17,7 +17,7 @@ import { import { Gap, Icon, TimeAgo, Tip } from "@cocalc/frontend/components"; import MostlyStaticMarkdown from "@cocalc/frontend/editors/slate/mostly-static-markdown"; import { IS_TOUCH } from "@cocalc/frontend/feature"; -import { modelToName } from "@cocalc/frontend/frame-editors/chatgpt/model-switch"; +import { modelToName } from "@cocalc/frontend/frame-editors/llm/model-switch"; import { COLORS } from "@cocalc/util/theme"; import { ChatActions } from "./actions"; import { getUserName } from "./chat-log"; diff --git a/src/packages/frontend/codemirror/extensions/ai-formula.tsx b/src/packages/frontend/codemirror/extensions/ai-formula.tsx index 3c982090dab..71fc33ce5e1 100644 --- a/src/packages/frontend/codemirror/extensions/ai-formula.tsx +++ b/src/packages/frontend/codemirror/extensions/ai-formula.tsx @@ -1,23 +1,28 @@ import { Button, Divider, Input, Modal, Space } from "antd"; import { useLanguageModelSetting } from "@cocalc/frontend/account/useLanguageModelSetting"; -import { redux, useEffect, useState } from "@cocalc/frontend/app-framework"; +import { + redux, + useEffect, + useState, + useTypedRedux, +} from "@cocalc/frontend/app-framework"; import { HelpIcon, Markdown, Paragraph, - Title, Text, + Title, } from "@cocalc/frontend/components"; import { LanguageModelVendorAvatar } from "@cocalc/frontend/components/language-model-icon"; import ModelSwitch, { modelToName, -} from "@cocalc/frontend/frame-editors/chatgpt/model-switch"; +} from "@cocalc/frontend/frame-editors/llm/model-switch"; import { show_react_modal } from "@cocalc/frontend/misc"; +import track from "@cocalc/frontend/user-tracking"; import { webapp_client } from "@cocalc/frontend/webapp-client"; +import { isFreeModel, isLanguageModel } from "@cocalc/util/db-schema/openai"; import { unreachable } from "@cocalc/util/misc"; -import { isFreeModel } from "@cocalc/util/db-schema/openai"; -import track from "@cocalc/frontend/user-tracking"; type Mode = "tex" | "md"; @@ -47,6 +52,7 @@ function AiGenFormula({ mode, text = "", project_id, cb }: Props) { const [formula, setFormula] = useState(""); const [generating, setGenerating] = useState(false); const [error, setError] = useState(undefined); + const ollama = useTypedRedux("customize", "ollama"); const enabled = redux .getStore("projects") @@ -134,12 +140,23 @@ function AiGenFormula({ mode, text = "", project_id, cb }: Props) { } }, [text]); + function renderModel2Name(): string { + if (isLanguageModel(model)) { + return modelToName(model); + } + const om = ollama?.get(model); + if (om) { + return om.get("title") ?? `Ollama ${model}`; + } + return model; + } + function renderTitle() { return ( <> <LanguageModelVendorAvatar model={model} /> Generate LaTeX Formula - using {modelToName(model)} + using {renderModel2Name()} {enabled ? ( <> diff --git a/src/packages/frontend/customize.tsx b/src/packages/frontend/customize.tsx index 9171a641a67..edfe62c02dc 100644 --- a/src/packages/frontend/customize.tsx +++ b/src/packages/frontend/customize.tsx @@ -8,6 +8,7 @@ import { fromJS, List, Map } from "immutable"; import { join } from "path"; + import { Actions, rclass, @@ -22,13 +23,14 @@ import { import { A, build_date, + Gap, Loading, r_join, smc_git_rev, smc_version, - Gap, UNIT, } from "@cocalc/frontend/components"; +import { getGoogleCloudImages, getImages } from "@cocalc/frontend/compute/api"; import { appBasePath } from "@cocalc/frontend/customize/app-base-path"; import { callback2, retry_until_success } from "@cocalc/util/async-utils"; import { @@ -37,6 +39,10 @@ import { FALLBACK_SOFTWARE_ENV, } from "@cocalc/util/compute-images"; import { DEFAULT_COMPUTE_IMAGE } from "@cocalc/util/db-schema"; +import type { + GoogleCloudImages, + Images, +} from "@cocalc/util/db-schema/compute-servers"; import { KUCALC_COCALC_COM, KUCALC_DISABLED, @@ -44,16 +50,12 @@ import { site_settings_conf, } from "@cocalc/util/db-schema/site-defaults"; import { deep_copy, dict, YEAR } from "@cocalc/util/misc"; +import { reuseInFlight } from "@cocalc/util/reuse-in-flight"; import { sanitizeSoftwareEnv } from "@cocalc/util/sanitize-software-envs"; import * as theme from "@cocalc/util/theme"; +import { OllamaPublic } from "@cocalc/util/types/llm"; import { DefaultQuotaSetting, Upgrades } from "@cocalc/util/upgrades/quota"; export { TermsOfService } from "@cocalc/frontend/customize/terms-of-service"; -import type { - GoogleCloudImages, - Images, -} from "@cocalc/util/db-schema/compute-servers"; -import { getImages, getGoogleCloudImages } from "@cocalc/frontend/compute/api"; -import { reuseInFlight } from "@cocalc/util/reuse-in-flight"; // this sets UI modes for using a kubernetes based back-end // 'yes' (historic value) equals 'cocalc.com' @@ -93,6 +95,8 @@ export type SoftwareEnvironments = TypedMap<{ export interface CustomizeState { is_commercial: boolean; openai_enabled: boolean; + google_vertexai_enabled: boolean; + ollama_enabled: boolean; neural_search_enabled: boolean; datastore: boolean; ssh_gateway: boolean; @@ -148,6 +152,8 @@ export interface CustomizeState { compute_servers_dns?: string; compute_servers_images?: TypedMap | string | null; compute_servers_images_google?: TypedMap | string | null; + + ollama?: TypedMap<{ [key: string]: TypedMap }>; } export class CustomizeStore extends Store { @@ -238,10 +244,12 @@ async function init_customize() { registration, strategies, software = null, + ollama = null, // the derived public information } = customize; process_kucalc(configuration); process_software(software, configuration.is_cocalc_com); process_customize(configuration); // this sets _is_configured to true + process_ollama(ollama); const actions = redux.getActions("account"); // Which account creation strategies we support. actions.setState({ strategies }); @@ -251,6 +259,12 @@ async function init_customize() { init_customize(); +function process_ollama(ollama) { + if (ollama) { + actions.setState({ ollama: fromJS(ollama) }); + } +} + function process_kucalc(obj) { // TODO make this a to_val function in site_settings_conf.kucalc obj.kucalc = validate_kucalc(obj.kucalc); diff --git a/src/packages/frontend/frame-editors/code-editor/actions.ts b/src/packages/frontend/frame-editors/code-editor/actions.ts index 40026b4b1e7..286cc8801b2 100644 --- a/src/packages/frontend/frame-editors/code-editor/actions.ts +++ b/src/packages/frontend/frame-editors/code-editor/actions.ts @@ -64,8 +64,8 @@ import { len, uuid, } from "@cocalc/util/misc"; -import languageModelCreateChat, { Options } from "../chatgpt/create-chat"; -import type { Scope as LanguageModelScope } from "../chatgpt/types"; +import languageModelCreateChat, { Options } from "../llm/create-chat"; +import type { Scope as LanguageModelScope } from "../llm/types"; import { default_opts } from "../codemirror/cm-options"; import { print_code } from "../frame-tree/print-code"; import * as tree_ops from "../frame-tree/tree-ops"; diff --git a/src/packages/frontend/frame-editors/frame-tree/format-error.tsx b/src/packages/frontend/frame-editors/frame-tree/format-error.tsx index 949812e01da..fbc4d248d63 100644 --- a/src/packages/frontend/frame-editors/frame-tree/format-error.tsx +++ b/src/packages/frontend/frame-editors/frame-tree/format-error.tsx @@ -1,11 +1,12 @@ // A dismissable error message that appears when formatting code. -import { useMemo } from "react"; import { Alert, Button } from "antd"; +import { useMemo } from "react"; + +import { file_associations } from "@cocalc/frontend/file-associations"; import { useFrameContext } from "@cocalc/frontend/frame-editors/frame-tree/frame-context"; +import HelpMeFix from "@cocalc/frontend/frame-editors/llm/help-me-fix"; import { CodeMirrorStatic } from "@cocalc/frontend/jupyter/codemirror-static"; -import HelpMeFix from "@cocalc/frontend/frame-editors/chatgpt/help-me-fix"; -import { file_associations } from "@cocalc/frontend/file-associations"; interface Props { formatError: string; @@ -14,10 +15,13 @@ interface Props { export default function FormatError({ formatError, formatInput }: Props) { const { actions } = useFrameContext(); - const language = useMemo(() => actions?.languageModelGetLanguage(), [actions]); + const language = useMemo( + () => actions?.languageModelGetLanguage(), + [actions], + ); const mode = useMemo( () => file_associations[language]?.opts?.mode ?? language, - [language] + [language], ); if (actions == null) return null; diff --git a/src/packages/frontend/frame-editors/frame-tree/title-bar.tsx b/src/packages/frontend/frame-editors/frame-tree/title-bar.tsx index 9919bb49c0b..c123495e3a8 100644 --- a/src/packages/frontend/frame-editors/frame-tree/title-bar.tsx +++ b/src/packages/frontend/frame-editors/frame-tree/title-bar.tsx @@ -34,7 +34,7 @@ import { Actions } from "../code-editor/actions"; import { is_safari } from "../generic/browser"; import { SaveButton } from "./save-button"; import { ConnectionStatus, EditorDescription, EditorSpec } from "./types"; -import LanguageModelTitleBarButton from "../chatgpt/title-bar-button"; +import LanguageModelTitleBarButton from "../llm/title-bar-button"; import userTracking from "@cocalc/frontend/user-tracking"; import TitleBarTour from "./title-bar-tour"; import { IS_MOBILE } from "@cocalc/frontend/feature"; diff --git a/src/packages/frontend/frame-editors/latex-editor/errors-and-warnings.tsx b/src/packages/frontend/frame-editors/latex-editor/errors-and-warnings.tsx index 31a36ade7d4..358e636608b 100644 --- a/src/packages/frontend/frame-editors/latex-editor/errors-and-warnings.tsx +++ b/src/packages/frontend/frame-editors/latex-editor/errors-and-warnings.tsx @@ -18,7 +18,7 @@ import { useRedux, } from "@cocalc/frontend/app-framework"; import { Icon, IconName, Loading } from "@cocalc/frontend/components"; -import HelpMeFix from "@cocalc/frontend/frame-editors/chatgpt/help-me-fix"; +import HelpMeFix from "@cocalc/frontend/frame-editors/llm/help-me-fix"; import { capitalize, is_different, path_split } from "@cocalc/util/misc"; import { COLORS } from "@cocalc/util/theme"; import { EditorState } from "../frame-tree/types"; diff --git a/src/packages/frontend/frame-editors/latex-editor/gutters.tsx b/src/packages/frontend/frame-editors/latex-editor/gutters.tsx index ed00bcb3005..e6b0eb8c7f7 100644 --- a/src/packages/frontend/frame-editors/latex-editor/gutters.tsx +++ b/src/packages/frontend/frame-editors/latex-editor/gutters.tsx @@ -9,12 +9,13 @@ // one gets a gutter mark, with pref to errors. The main error log shows everything, so this should be OK. import { Popover } from "antd"; -import { capitalize } from "@cocalc/util/misc"; + import { Icon } from "@cocalc/frontend/components"; -import { SPEC, SpecItem } from "./errors-and-warnings"; -import { IProcessedLatexLog, Error } from "./latex-log-parser"; -import HelpMeFix from "@cocalc/frontend/frame-editors/chatgpt/help-me-fix"; +import HelpMeFix from "@cocalc/frontend/frame-editors/llm/help-me-fix"; +import { capitalize } from "@cocalc/util/misc"; import { Actions } from "../code-editor/actions"; +import { SPEC, SpecItem } from "./errors-and-warnings"; +import { Error, IProcessedLatexLog } from "./latex-log-parser"; export function update_gutters(opts: { log: IProcessedLatexLog; diff --git a/src/packages/frontend/frame-editors/chatgpt/context.tsx b/src/packages/frontend/frame-editors/llm/context.tsx similarity index 100% rename from src/packages/frontend/frame-editors/chatgpt/context.tsx rename to src/packages/frontend/frame-editors/llm/context.tsx diff --git a/src/packages/frontend/frame-editors/chatgpt/create-chat.ts b/src/packages/frontend/frame-editors/llm/create-chat.ts similarity index 100% rename from src/packages/frontend/frame-editors/chatgpt/create-chat.ts rename to src/packages/frontend/frame-editors/llm/create-chat.ts diff --git a/src/packages/frontend/frame-editors/chatgpt/help-me-fix.tsx b/src/packages/frontend/frame-editors/llm/help-me-fix.tsx similarity index 100% rename from src/packages/frontend/frame-editors/chatgpt/help-me-fix.tsx rename to src/packages/frontend/frame-editors/llm/help-me-fix.tsx diff --git a/src/packages/frontend/frame-editors/chatgpt/model-switch.tsx b/src/packages/frontend/frame-editors/llm/model-switch.tsx similarity index 80% rename from src/packages/frontend/frame-editors/chatgpt/model-switch.tsx rename to src/packages/frontend/frame-editors/llm/model-switch.tsx index bee9d20daef..a99d6f66217 100644 --- a/src/packages/frontend/frame-editors/chatgpt/model-switch.tsx +++ b/src/packages/frontend/frame-editors/llm/model-switch.tsx @@ -1,6 +1,6 @@ import { Radio, Tooltip } from "antd"; -import { CSS, redux } from "@cocalc/frontend/app-framework"; +import { CSS, redux, useTypedRedux } from "@cocalc/frontend/app-framework"; import { DEFAULT_MODEL, LLM_USERNAMES, @@ -8,14 +8,15 @@ import { USER_SELECTABLE_LANGUAGE_MODELS, isFreeModel, model2service, + toOllamaModel, } from "@cocalc/util/db-schema/openai"; export { DEFAULT_MODEL }; export type { LanguageModel }; interface Props { - model: LanguageModel; - setModel: (model: LanguageModel) => void; + model: LanguageModel | string; + setModel: (model: LanguageModel | string) => void; size?; style?: CSS; project_id: string; @@ -45,6 +46,12 @@ export default function ModelSwitch({ undefined, "google", ); + const showOllama = projectsStore.hasLanguageModelEnabled( + project_id, + undefined, + "ollama", + ); + const ollama = useTypedRedux("customize", "ollama"); function renderLLMButton(btnModel: LanguageModel, title: string) { if (!USER_SELECTABLE_LANGUAGE_MODELS.includes(btnModel)) return; @@ -98,6 +105,21 @@ export default function ModelSwitch({ ); } + function renderOllama() { + if (!showOllama || !ollama) return null; + + return Object.entries(ollama.toJS()).map(([key, config]) => { + const title = config.display ?? `Ollama: ${key}`; + return ( + + {title} + + ); + }); + } + + console.log("model", model); + // all models selectable here must be in util/db-schema/openai::USER_SELECTABLE_LANGUAGE_MODELS return ( {renderOpenAI()} {renderGoogle()} + {renderOllama()} ); } diff --git a/src/packages/frontend/frame-editors/chatgpt/shorten-error.ts b/src/packages/frontend/frame-editors/llm/shorten-error.ts similarity index 100% rename from src/packages/frontend/frame-editors/chatgpt/shorten-error.ts rename to src/packages/frontend/frame-editors/llm/shorten-error.ts diff --git a/src/packages/frontend/frame-editors/chatgpt/title-bar-button-tour.tsx b/src/packages/frontend/frame-editors/llm/title-bar-button-tour.tsx similarity index 100% rename from src/packages/frontend/frame-editors/chatgpt/title-bar-button-tour.tsx rename to src/packages/frontend/frame-editors/llm/title-bar-button-tour.tsx diff --git a/src/packages/frontend/frame-editors/chatgpt/title-bar-button.tsx b/src/packages/frontend/frame-editors/llm/title-bar-button.tsx similarity index 100% rename from src/packages/frontend/frame-editors/chatgpt/title-bar-button.tsx rename to src/packages/frontend/frame-editors/llm/title-bar-button.tsx diff --git a/src/packages/frontend/frame-editors/chatgpt/types.ts b/src/packages/frontend/frame-editors/llm/types.ts similarity index 100% rename from src/packages/frontend/frame-editors/chatgpt/types.ts rename to src/packages/frontend/frame-editors/llm/types.ts diff --git a/src/packages/frontend/jupyter/chatgpt/error.tsx b/src/packages/frontend/jupyter/chatgpt/error.tsx index 58866ab9a99..a459a11303b 100644 --- a/src/packages/frontend/jupyter/chatgpt/error.tsx +++ b/src/packages/frontend/jupyter/chatgpt/error.tsx @@ -3,8 +3,9 @@ Use ChatGPT to explain an error message and help the user fix it. */ import { CSSProperties } from "react"; -import HelpMeFix from "@cocalc/frontend/frame-editors/chatgpt/help-me-fix"; + import { useFrameContext } from "@cocalc/frontend/frame-editors/frame-tree/frame-context"; +import HelpMeFix from "@cocalc/frontend/frame-editors/llm/help-me-fix"; interface Props { style?: CSSProperties; diff --git a/src/packages/frontend/jupyter/chatgpt/explain.tsx b/src/packages/frontend/jupyter/chatgpt/explain.tsx index 50e753a9b3f..40a388bc3db 100644 --- a/src/packages/frontend/jupyter/chatgpt/explain.tsx +++ b/src/packages/frontend/jupyter/chatgpt/explain.tsx @@ -12,12 +12,12 @@ import { Icon } from "@cocalc/frontend/components/icon"; import { LanguageModelVendorAvatar } from "@cocalc/frontend/components/language-model-icon"; import PopconfirmKeyboard from "@cocalc/frontend/components/popconfirm-keyboard"; import StaticMarkdown from "@cocalc/frontend/editors/slate/static-markdown"; +import { useFrameContext } from "@cocalc/frontend/frame-editors/frame-tree/frame-context"; import ModelSwitch, { LanguageModel, modelToMention, modelToName, -} from "@cocalc/frontend/frame-editors/chatgpt/model-switch"; -import { useFrameContext } from "@cocalc/frontend/frame-editors/frame-tree/frame-context"; +} from "@cocalc/frontend/frame-editors/llm/model-switch"; import { ProjectsStore } from "@cocalc/frontend/projects/store"; import type { JupyterActions } from "../browser-actions"; diff --git a/src/packages/frontend/jupyter/insert-cell/ai-cell-generator.tsx b/src/packages/frontend/jupyter/insert-cell/ai-cell-generator.tsx index a188396d8c3..b20fa35bb76 100644 --- a/src/packages/frontend/jupyter/insert-cell/ai-cell-generator.tsx +++ b/src/packages/frontend/jupyter/insert-cell/ai-cell-generator.tsx @@ -9,10 +9,10 @@ import { Paragraph } from "@cocalc/frontend/components"; import { Icon } from "@cocalc/frontend/components/icon"; import { LanguageModelVendorAvatar } from "@cocalc/frontend/components/language-model-icon"; import StaticMarkdown from "@cocalc/frontend/editors/slate/static-markdown"; +import { NotebookFrameActions } from "@cocalc/frontend/frame-editors/jupyter-editor/cell-notebook/actions"; import ModelSwitch, { modelToName, -} from "@cocalc/frontend/frame-editors/chatgpt/model-switch"; -import { NotebookFrameActions } from "@cocalc/frontend/frame-editors/jupyter-editor/cell-notebook/actions"; +} from "@cocalc/frontend/frame-editors/llm/model-switch"; import { splitCells } from "@cocalc/frontend/jupyter/chatgpt/split-cells"; import track from "@cocalc/frontend/user-tracking"; import { webapp_client } from "@cocalc/frontend/webapp-client"; diff --git a/src/packages/frontend/project/page/home-page/ai-generate-jupyter.tsx b/src/packages/frontend/project/page/home-page/ai-generate-jupyter.tsx index cd4d8d32c25..04016fcb5e7 100644 --- a/src/packages/frontend/project/page/home-page/ai-generate-jupyter.tsx +++ b/src/packages/frontend/project/page/home-page/ai-generate-jupyter.tsx @@ -34,11 +34,11 @@ import { LanguageModelVendorAvatar } from "@cocalc/frontend/components/language- import ProgressEstimate from "@cocalc/frontend/components/progress-estimate"; import SelectKernel from "@cocalc/frontend/components/run-button/select-kernel"; import StaticMarkdown from "@cocalc/frontend/editors/slate/static-markdown"; -import ModelSwitch, { - modelToName, -} from "@cocalc/frontend/frame-editors/chatgpt/model-switch"; import type { JupyterEditorActions } from "@cocalc/frontend/frame-editors/jupyter-editor/actions"; import { NotebookFrameActions } from "@cocalc/frontend/frame-editors/jupyter-editor/cell-notebook/actions"; +import ModelSwitch, { + modelToName, +} from "@cocalc/frontend/frame-editors/llm/model-switch"; import { splitCells } from "@cocalc/frontend/jupyter/chatgpt/split-cells"; import getKernelSpec from "@cocalc/frontend/jupyter/kernelspecs"; import { StartButton } from "@cocalc/frontend/project/start-button"; diff --git a/src/packages/frontend/projects/store.ts b/src/packages/frontend/projects/store.ts index f443a0b8780..29c1adfc29d 100644 --- a/src/packages/frontend/projects/store.ts +++ b/src/packages/frontend/projects/store.ts @@ -734,10 +734,22 @@ export class ProjectsStore extends Store { openAICache.clear(); } + public llmEnabledSummary(project_id: string = "global", tag?: string) { + const haveOpenAI = this.hasLanguageModelEnabled(project_id, tag, "openai"); + const haveGoogle = this.hasLanguageModelEnabled(project_id, tag, "google"); + const haveOllama = this.hasLanguageModelEnabled(project_id, tag, "ollama"); + + return { + openai: haveOpenAI, + google: haveGoogle, + ollama: haveOllama, + }; + } + hasLanguageModelEnabled( project_id: string = "global", tag?: string, - vendor: "openai" | "google" | "any" = "any", + vendor: "openai" | "google" | "ollama" | "any" = "any", ): boolean { // cache answer for a few seconds, in case this gets called a lot: @@ -769,17 +781,19 @@ export class ProjectsStore extends Store { private _hasLanguageModelEnabled( project_id: string | "global" = "global", courseLimited?: boolean, - vendor: "openai" | "google" | "any" = "any", + vendor: "openai" | "google" | "ollama" | "any" = "any", ): boolean { const customize = redux.getStore("customize"); const haveOpenAI = customize.get("openai_enabled"); const haveGoogle = customize.get("google_vertexai_enabled"); + const haveOllama = customize.get("ollama_enabled"); - if (!haveOpenAI && !haveGoogle) return false; // the vendor == "any" case + if (!haveOpenAI && !haveGoogle && !haveOllama) return false; // the vendor == "any" case if (vendor === "openai" && !haveOpenAI) return false; if (vendor === "google" && !haveGoogle) return false; + if (vendor === "ollama" && !haveOllama) return false; - // this customization accounts for disabling any language model vendor + // this customization parameter accounts for disabling **any** language model vendor const openai_disabled = redux .getStore("account") .getIn(["other_settings", "openai_disabled"]); diff --git a/src/packages/frontend/sagews/chatgpt.ts b/src/packages/frontend/sagews/chatgpt.ts index fe46bbd7ea5..0d58580c497 100644 --- a/src/packages/frontend/sagews/chatgpt.ts +++ b/src/packages/frontend/sagews/chatgpt.ts @@ -1,5 +1,5 @@ import { redux } from "@cocalc/frontend/app-framework"; -import { getHelp } from "@cocalc/frontend/frame-editors/chatgpt/help-me-fix"; +import { getHelp } from "@cocalc/frontend/frame-editors/llm/help-me-fix"; import { getValidLanguageModelName } from "@cocalc/util/db-schema/openai"; import { MARKERS } from "@cocalc/util/sagews"; diff --git a/src/packages/hub/servers/server-settings.ts b/src/packages/hub/servers/server-settings.ts index b1068bb2125..ba3150098ba 100644 --- a/src/packages/hub/servers/server-settings.ts +++ b/src/packages/hub/servers/server-settings.ts @@ -7,11 +7,13 @@ Synchronized table that tracks server settings. */ +import { isEmpty } from "lodash"; + import { once } from "@cocalc/util/async-utils"; import { EXTRAS as SERVER_SETTINGS_EXTRAS } from "@cocalc/util/db-schema/site-settings-extras"; +import { AllSiteSettings } from "@cocalc/util/db-schema/types"; import { startswith } from "@cocalc/util/misc"; import { site_settings_conf as SITE_SETTINGS_CONF } from "@cocalc/util/schema"; -import { isEmpty } from "lodash"; import { database } from "./database"; // Returns: @@ -22,16 +24,16 @@ import { database } from "./database"; // - table: the table, so you can watch for on change events... // These get automatically updated when the database changes. -interface ServerSettings { - all: object; +export interface ServerSettingsDynamic { + all: AllSiteSettings; pub: object; version: object; table: any; } -let serverSettings: ServerSettings | undefined = undefined; +let serverSettings: ServerSettingsDynamic | undefined = undefined; -export default async function getServerSettings(): Promise { +export default async function getServerSettings(): Promise { if (serverSettings != null) { return serverSettings; } diff --git a/src/packages/hub/webapp-configuration.ts b/src/packages/hub/webapp-configuration.ts index 9b01b8d21fa..43a3fdb1735 100644 --- a/src/packages/hub/webapp-configuration.ts +++ b/src/packages/hub/webapp-configuration.ts @@ -11,25 +11,29 @@ import { delay } from "awaiting"; import debug from "debug"; +import { isEmpty } from "lodash"; +import LRU from "lru-cache"; import type { PostgreSQL } from "@cocalc/database/postgres/types"; +import { get_passport_manager, PassportManager } from "@cocalc/server/hub/auth"; import { getSoftwareEnvironments } from "@cocalc/server/software-envs"; import { callback2 as cb2 } from "@cocalc/util/async-utils"; import { EXTRAS as SERVER_SETTINGS_EXTRAS } from "@cocalc/util/db-schema/site-settings-extras"; import { SoftwareEnvConfig } from "@cocalc/util/sanitize-software-envs"; import { site_settings_conf as SITE_SETTINGS_CONF } from "@cocalc/util/schema"; +import { OllamaPublic } from "@cocalc/util/types/llm"; import { parseDomain, ParseResultType } from "parse-domain"; -import { get_passport_manager, PassportManager } from "@cocalc/server/hub/auth"; -import getServerSettings from "./servers/server-settings"; +import getServerSettings, { + ServerSettingsDynamic, +} from "./servers/server-settings"; import { have_active_registration_tokens } from "./utils"; const L = debug("hub:webapp-config"); -import LRU from "lru-cache"; const CACHE = new LRU({ max: 1000, ttl: 60 * 1000 }); // 1 minutes export function clear_cache(): void { - CACHE.reset(); + CACHE.clear(); } type Theme = { [key: string]: string | boolean }; @@ -40,6 +44,7 @@ interface Config { registration: any; strategies: object; software: SoftwareEnvConfig | null; + ollama: { [key: string]: OllamaPublic }; } async function get_passport_manager_async(): Promise { @@ -53,7 +58,7 @@ async function get_passport_manager_async(): Promise { return pp_manager; } else { L( - `WARNING: Passport Manager not available yet -- trying again in ${ms}ms` + `WARNING: Passport Manager not available yet -- trying again in ${ms}ms`, ); await delay(ms); ms = Math.min(10000, 1.3 * ms); @@ -63,7 +68,7 @@ async function get_passport_manager_async(): Promise { export class WebappConfiguration { private readonly db: PostgreSQL; - private data?: any; + private data?: ServerSettingsDynamic; constructor({ db }) { this.db = db; @@ -168,14 +173,43 @@ export class WebappConfiguration { return strategies as object; } + // derives the public ollama model configuration from the private one + private get_ollama_public(): { [key: string]: OllamaPublic } { + if (this.data == null) { + throw new Error("server settings not yet initialized"); + } + const ollama = this.data.all.ollama_configuration; + if (isEmpty(ollama)) return {}; + + const public_ollama = {}; + for (const key in ollama) { + const conf = ollama[key]; + const cocalc = conf.cocalc ?? {}; + const model = conf.model ?? key; + public_ollama[key] = { + key, + model, + display: cocalc.display ?? `Ollama ${model}`, + icon: cocalc.icon, // fallback is the Ollama icon, frontend does that + }; + } + return public_ollama; + } + private async get_config({ country, host }): Promise { - const [configuration, registration, software] = await Promise.all([ + while (this.data == null) { + L.debug("waiting for server settings to be initialized"); + await delay(100); + } + + const [configuration, registration, software, ollama] = await Promise.all([ this.get_configuration({ host, country }), have_active_registration_tokens(this.db), getSoftwareEnvironments("webapp"), + this.get_ollama_public(), ]); const strategies = await this.get_strategies(); - return { configuration, registration, strategies, software }; + return { configuration, registration, strategies, software, ollama }; } // it returns a shallow copy, hence you can modify/add keys in the returned map! diff --git a/src/packages/server/llm/call-chatgpt.ts b/src/packages/server/llm/call-llm.ts similarity index 98% rename from src/packages/server/llm/call-chatgpt.ts rename to src/packages/server/llm/call-llm.ts index 4763e260b6c..a807d26237a 100644 --- a/src/packages/server/llm/call-chatgpt.ts +++ b/src/packages/server/llm/call-llm.ts @@ -7,7 +7,7 @@ import { ChatOutput } from "@cocalc/util/types/llm"; import { Stream } from "openai/streaming"; import { totalNumTokens } from "./chatgpt-numtokens"; -const log = getLogger("llm:call-chatgpt"); +const log = getLogger("llm:call-llm"); interface CallChatGPTOpts { openai: OpenAI; diff --git a/src/packages/server/llm/client.ts b/src/packages/server/llm/client.ts index 1f50dab0c0f..79a0bff122b 100644 --- a/src/packages/server/llm/client.ts +++ b/src/packages/server/llm/client.ts @@ -5,13 +5,15 @@ You do not have to worry too much about throwing an exception, because they're c */ import OpenAI from "openai"; +import jsonStable from "json-stable-stringify"; +import { Ollama } from "@langchain/community/llms/ollama"; +import * as _ from "lodash"; import getLogger from "@cocalc/backend/logger"; import { getServerSettings } from "@cocalc/database/settings/server-settings"; import { LanguageModel, model2vendor } from "@cocalc/util/db-schema/openai"; import { unreachable } from "@cocalc/util/misc"; import { VertexAIClient } from "./vertex-ai-client"; -import { Ollama } from "@langchain/community/llms/ollama"; const log = getLogger("llm:client"); @@ -68,29 +70,59 @@ export async function getClient( const ollamaCache: { [key: string]: Ollama } = {}; +/** + * The idea here is: the ollama config contains all available endpoints and their configuration. + * The "model" is the unique key in the ollama_configuration mapping, it was prefixed by "ollama-". + * For the actual Ollama client instantitation, we pick the model parameter from the config or just use the unique model name as a fallback. + * In particular, this means you can query the same Ollama model with differnet parameters, or even have several ollama servers running. + * All other config parameters are passed to the Ollama constructor (e.g. topK, temperature, etc.). + */ export async function getOllama(model: string) { - // model is the unique key in the ServerSettings.ollama_configuration mapping - if (ollamaCache[model]) { - return ollamaCache[model]; + if (model.startsWith("ollama-")) { + throw new Error( + `At this point, the model name should no longer have the "ollama-" prefix`, + ); } const settings = await getServerSettings(); const config = settings.ollama_configuration?.[model]; if (!config) { throw new Error( - `Ollama model ${model} not configured – you have to create an entry {${model}: {url: "https://...", ...}} in the "Ollama Configuration" entry of the server settings`, + `Ollama model ${model} not configured – you have to create an entry {${model}: {baseUrl: "https://...", ...}} in the "Ollama Configuration" entry of the server settings!`, ); } - const baseUrl = config.url; + // the key is a hash of the model name and the specific config – such that changes in the config will invalidate the cache + const key = `${model}:${jsonStable(config)}`; + + // model is the unique key in the ServerSettings.ollama_configuration mapping + if (ollamaCache[key]) { + log.debug(`Using cached Ollama client for model ${model}`); + return ollamaCache[key]; + } + + const baseUrl = config.baseUrl; if (!baseUrl) { - throw new Error(`The url of the Ollama model ${model} is not configured`); + throw new Error( + `The "baseUrl" field of the Ollama model ${model} is not configured`, + ); } const keepAlive = config.keepAlive ?? -1; - const client = new Ollama({ baseUrl, model, keepAlive }); - ollamaCache[model] = client; + // extract all other properties from the config, except the url, model, keepAlive field and the "cocalc" field + const other = _.omit(config, ["baseUrl", "model", "keepAlive", "cocalc"]); + const ollamaConfig = { + baseUrl, + model: config.model ?? model, + keepAlive, + ...other, + }; + + log.debug("Instantiating Ollama client with config", ollamaConfig); + + const client = new Ollama(ollamaConfig); + ollamaCache[key] = client; return client; } diff --git a/src/packages/server/llm/index.ts b/src/packages/server/llm/index.ts index 8f2800d53e2..dcd4bbbc834 100644 --- a/src/packages/server/llm/index.ts +++ b/src/packages/server/llm/index.ts @@ -30,7 +30,7 @@ import { } from "@cocalc/util/db-schema/openai"; import { ChatOptions, ChatOutput, History } from "@cocalc/util/types/llm"; import { checkForAbuse } from "./abuse"; -import { callChatGPTAPI } from "./call-chatgpt"; +import { callChatGPTAPI } from "./call-llm"; import { getClient } from "./client"; import { saveResponse } from "./save-response"; import { VertexAIClient } from "./vertex-ai-client"; diff --git a/src/packages/util/db-schema/openai.ts b/src/packages/util/db-schema/openai.ts index 372f47be07b..e4308ae990f 100644 --- a/src/packages/util/db-schema/openai.ts +++ b/src/packages/util/db-schema/openai.ts @@ -43,16 +43,26 @@ export function isLanguageModel(model?: string): model is LanguageModel { export function getValidLanguageModelName( model: string | undefined, - filter: { google: boolean; openai: boolean } = { google: true, openai: true }, -): LanguageModel { - const dftl = filter.openai === true ? DEFAULT_MODEL : "chat-bison-001"; + filter: { google: boolean; openai: boolean; ollama: boolean } = { + google: true, + openai: true, + ollama: false, + }, + ollama: string[], // keys of ollama models +): LanguageModel | string { + const dftl = + filter.openai === true + ? DEFAULT_MODEL + : filter.ollama && ollama?.length > 0 + ? toOllamaModel(ollama[0]) + : "chat-bison-001"; if (model == null) { return dftl; } if (!LANGUAGE_MODELS.includes(model as LanguageModel)) { return dftl; } - return model as LanguageModel; + return model; } export interface OpenAIMessage { @@ -129,6 +139,18 @@ export function model2vendor(model: LanguageModel): Vendor { } } +export function toOllamaModel(model: string) { + return `ollama-${model}`; +} + +export function fromOllamaModel(model: string) { + return model.replace(/^ollama-/, ""); +} + +export function isOllamaLLM(model: string) { + return model.startsWith("ollama-"); +} + const MODELS_OPENAI = [ "gpt-3.5-turbo", "gpt-3.5-turbo-16k", @@ -166,13 +188,14 @@ export const LLM_USERNAMES = { "gemini-pro": "Gemini Pro", } as const; -export function isFreeModel(model: Model) { +export function isFreeModel(model: string) { + if (!LANGUAGE_MODELS.includes(model as LanguageModel)) return false; return ( - model == "gpt-3.5-turbo" || - model == "text-bison-001" || - model == "chat-bison-001" || - model == "embedding-gecko-001" || - model == "gemini-pro" + (model as Model) == "gpt-3.5-turbo" || + (model as Model) == "text-bison-001" || + (model as Model) == "chat-bison-001" || + (model as Model) == "embedding-gecko-001" || + (model as Model) == "gemini-pro" ); } diff --git a/src/packages/util/db-schema/site-settings-extras.ts b/src/packages/util/db-schema/site-settings-extras.ts index b338ae55074..4557aa0a64a 100644 --- a/src/packages/util/db-schema/site-settings-extras.ts +++ b/src/packages/util/db-schema/site-settings-extras.ts @@ -9,31 +9,31 @@ // You can use markdown in the descriptions below and it is rendered properly! +import { isValidUUID } from "@cocalc/util/misc"; import { Config, + SiteSettings, + displayJson, + from_json, is_email_enabled, - only_for_smtp, - only_for_sendgrid, - only_for_password_reset_smtp, - to_bool, - only_booleans, - to_int, - only_nonneg_int, - toFloat, onlyNonnegFloat, onlyPosFloat, - only_pos_int, - only_commercial, + only_booleans, only_cocalc_com, - from_json, + only_commercial, + only_for_password_reset_smtp, + only_for_sendgrid, + only_for_smtp, + only_nonneg_int, + only_pos_int, parsableJson, - displayJson, + toFloat, + to_bool, + to_int, to_trimmed_str, - SiteSettings, } from "./site-defaults"; -import { isValidUUID } from "@cocalc/util/misc"; -import { is_valid_email_address, expire_time } from "@cocalc/util/misc"; +import { expire_time, is_valid_email_address } from "@cocalc/util/misc"; export const pii_retention_parse = (retention: string): number | false => { if (retention == "never" || retention == null) return false; @@ -84,6 +84,80 @@ const neural_search_enabled = (conf: SiteSettings) => const jupyter_api_enabled = (conf: SiteSettings) => to_bool(conf.jupyter_api_enabled); +function ollama_valid(value: string): boolean { + if (!parsableJson(value)) { + return false; + } + const obj = from_json(value); + if (typeof obj !== "object") { + return false; + } + for (const key in obj) { + const val = obj[key] as any; + if (typeof val !== "object") { + return false; + } + if (typeof val.baseUrl !== "string") { + return false; + } + if (val.model && typeof val.model !== "string") { + return false; + } + const c = val.cocalc; + if (c != null) { + if (typeof c !== "object") { + return false; + } + if (c.display && typeof c.display !== "string") { + return false; + } + if (c.enabled && typeof c.enabled !== "boolean") { + return false; + } + } + } + return true; +} + +function ollama_display(value: string): string { + if (!parsableJson(value)) { + return "Ollama JSON not parseable. Must be {[key : string] : {model: string, baseUrL: string, cocalc: {display: string, ...}, ...}"; + } + const obj = from_json(value); + if (typeof obj !== "object") { + return "Ollama JSON must be an object"; + } + const ret: string[] = []; + for (const key in obj) { + const val = obj[key] as any; + if (typeof val !== "object") { + return `Ollama config ${key} must be an object`; + } + if (typeof val.baseUrl !== "string") { + return `Ollama config ${key} baseUrl field must be a string`; + } + if (val.model && typeof val.model !== "string") { + return `Ollama config ${key} model field must be a string`; + } + const c = val.cocalc; + if (c != null) { + if (typeof c !== "object") { + return `Ollama config ${key} cocalc field must be an object`; + } + if (c.display && typeof c.display !== "string") { + return `Ollama config ${key} cocalc.display field must be a string`; + } + if (c.enabled && typeof c.enabled !== "boolean") { + return `Ollama config ${key} cocalc.enabled field must be a boolean`; + } + } + ret.push( + `Olama ${key} at ${val.baseUrl} named ${c?.display ?? val.model ?? key}`, + ); + } + return `[${ret.join(", ")}]`; +} + export type SiteSettingsExtrasKeys = | "pii_retention" | "stripe_heading" @@ -189,7 +263,8 @@ export const EXTRAS: SettingsExtras = { multiline: 5, show: ollama_enabled, to_val: from_json, - valid: parsableJson, + valid: ollama_valid, + to_display: ollama_display, }, qdrant_section: { name: "Qdrant Configuration", diff --git a/src/packages/util/types/llm.ts b/src/packages/util/types/llm.ts index c028ea6e37e..36c8d35668e 100644 --- a/src/packages/util/types/llm.ts +++ b/src/packages/util/types/llm.ts @@ -30,3 +30,10 @@ export interface ChatOptions { stream?: (output?: string) => void; maxTokens?: number; } + +export interface OllamaPublic { + key: string; // the key in the dict + model: string; + display: string; + icon: string; +}