diff --git a/next.config.mjs b/next.config.mjs index b6362cf26..2a3959ad8 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,10 +1,18 @@ +import { readFile } from 'node:fs/promises'; + +// Build information +process.env.NEXT_PUBLIC_BUILD_HASH = 'big-agi-2-dev'; +process.env.NEXT_PUBLIC_BUILD_PKGVER = JSON.parse('' + await readFile(new URL('./package.json', import.meta.url))).version; +process.env.NEXT_PUBLIC_BUILD_TIMESTAMP = new Date().toISOString(); +console.log(` 🧠 \x1b[1mbig-AGI\x1b[0m v${process.env.NEXT_PUBLIC_BUILD_PKGVER} (@${process.env.NEXT_PUBLIC_BUILD_HASH})`); + // Non-default build types const buildType = process.env.BIG_AGI_BUILD === 'standalone' ? 'standalone' : process.env.BIG_AGI_BUILD === 'static' ? 'export' : undefined; -buildType && console.log(` 🧠 big-AGI: building for ${buildType}...\n`); +buildType && console.log(` 🧠 big-AGI: building for ${buildType}...\n`); /** @type {import('next').NextConfig} */ let nextConfig = { diff --git a/package.json b/package.json index e620277eb..4ae4e6ae8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "big-agi", - "version": "1.16.0", + "version": "1.91.0", "private": true, "author": "Enrico Ros ", "repository": "https://github.com/enricoros/big-agi", diff --git a/pages/info/debug.tsx b/pages/info/debug.tsx index 391d38d35..d451a1507 100644 --- a/pages/info/debug.tsx +++ b/pages/info/debug.tsx @@ -15,9 +15,7 @@ import { withNextJSPerPageLayout } from '~/common/layout/withLayout'; // basics import { Brand } from '~/common/app.config'; import { ROUTE_APP_CHAT, ROUTE_INDEX } from '~/common/app.routes'; - -// apps access -import { incrementalNewsVersion } from '../../src/apps/news/news.version'; +import { Release } from '~/common/app.release'; // capabilities access import { useCapabilityBrowserSpeechRecognition, useCapabilityElevenLabs, useCapabilityTextToImage } from '~/common/components/useCapabilities'; @@ -30,7 +28,7 @@ import { useLogicSherpaStore } from '~/common/logic/store-logic-sherpa'; import { useUXLabsStore } from '~/common/state/store-ux-labs'; // utils access -import { BrowserLang, Is, clientHostName, isPwa } from '~/common/util/pwaUtils'; +import { BrowserLang, clientHostName, Is, isPwa } from '~/common/util/pwaUtils'; import { getGA4MeasurementId } from '~/common/components/GoogleAnalytics'; import { prettyTimestampForFilenames } from '~/common/util/timeUtils'; import { supportsClipboardRead } from '~/common/util/clipboardUtils'; @@ -71,6 +69,8 @@ function DebugJsonCard(props: { title: string, data: any }) { } +const frontendBuild = Release.buildInfo('frontend'); + function AppDebug() { // state @@ -103,11 +103,15 @@ function AppDebug() { chatsCount, foldersCount: folders?.length, foldersEnabled: enableFolders, - newsCurrent: incrementalNewsVersion, + newsCurrent: Release.Monotonics.NewsVersion, newsSeen: lastSeenNewsVersion, labsActive: uxLabsExperiments, reloads: usageCount, }, + release: { + app: Release.App, + build: frontendBuild, + }, }; const cBackend = { configuration: backendCaps, diff --git a/src/apps/news/news.data.tsx b/src/apps/news/news.data.tsx index 435e6214c..37c9a90ee 100644 --- a/src/apps/news/news.data.tsx +++ b/src/apps/news/news.data.tsx @@ -14,6 +14,7 @@ import { PerplexityIcon } from '~/common/components/icons/vendors/PerplexityIcon import { Brand } from '~/common/app.config'; import { Link } from '~/common/components/Link'; +import { Release } from '~/common/app.release'; import { clientUtmSource } from '~/common/util/pwaUtils'; import { platformAwareKeystrokes } from '~/common/components/KeyStroke'; @@ -52,8 +53,8 @@ interface NewsItem { // news and feature surfaces export const NewsItems: NewsItem[] = [ { - versionCode: '2.0.0-ea1', // 1.91.0 - versionName: 'Big-AGI V2 EA1', + versionCode: Release.App.versionCode, + versionName: Release.App.versionName, versionDate: new Date('2024-10-15T01:00:00Z'), items: [ { text: <>You're running an unsupported Early Access build of Big-AGI V2. This version is used by developers to implement long-term breaking features. }, diff --git a/src/apps/news/news.version.tsx b/src/apps/news/news.version.tsx deleted file mode 100644 index 775403fe0..000000000 --- a/src/apps/news/news.version.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// NOTE: this is a separate file to help with bundle tracing, as it's included by the ProviderBootstrapLogic (i.e. by All pages) - -// update this variable every time you want to broadcast a new version to clients -export const incrementalNewsVersion: number = 16.1; // not notifying for 1.16.8 diff --git a/src/common/app.release.ts b/src/common/app.release.ts new file mode 100644 index 000000000..42f53dd30 --- /dev/null +++ b/src/common/app.release.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2024 Enrico Ros + * + * This file is include by both the frontend and backend, however depending on the time + * of the build, the values may be different. + */ + + +/** + * We centralize here the version information of the app, to have a uniform configuration surface. + */ +export const Release = { + App: { + pl: 'gh-2', // do now use slashes here + versionCode: '2.0.0-ea1', // 1.91.0 sequentially... + versionName: 'Big-AGI V2 EA1', + }, + + // Future compatibility + Features: { + // ... + }, + + // this is here to trigger revalidation of data, e.g. models refresh + Monotonics: { + Aix: 1, + NewsVersion: 191, + }, + + /** + * We force explicit declaration of the caller. + */ + buildInfo: (_type: 'frontend' | 'backend') => ({ + // **NOTE**: do not change var names here, as they're matched from this point forward + // between the frontend and backend to ensure runtime consistency. + gitSha: process.env.NEXT_PUBLIC_BUILD_HASH, + pkgVersion: process.env.NEXT_PUBLIC_BUILD_PKGVER, + timestamp: process.env.NEXT_PUBLIC_BUILD_TIMESTAMP, + }), +}; diff --git a/src/common/logic/reconfigureBackendModels.ts b/src/common/logic/reconfigureBackendModels.ts new file mode 100644 index 000000000..516d5fc15 --- /dev/null +++ b/src/common/logic/reconfigureBackendModels.ts @@ -0,0 +1,86 @@ +import { createModelsServiceForVendor } from '~/modules/llms/vendors/vendor.helpers'; +import { findAllModelVendors } from '~/modules/llms/vendors/vendors.registry'; +import { getBackendCapabilities } from '~/modules/backend/store-backend-capabilities'; +import { llmsUpdateModelsForServiceOrThrow } from '~/modules/llms/llm.client'; + +import type { DModelsService, DModelsServiceId } from '~/common/stores/llms/modelsservice.types'; +import { llmsStoreActions, llmsStoreState } from '~/common/stores/llms/store-llms'; + + +// Note: this function is designed to be called once per session +let _isConfiguring = false; +let _isConfigurationDone = false; + + +/** + * Reload models for services configured in the backend. + */ +export async function reconfigureBackendModels(newLlmReconfigHash: string, setReconfigHash: (hash: string) => void) { + + // Note: double-calling is only expected to happen in react strict mode + if (_isConfiguring || _isConfigurationDone) + return; + + // skip if no change is detected / no config needed + const backendCaps = getBackendCapabilities(); + const llmReconfigHash = backendCaps.hashLlmReconfig; + if (!llmReconfigHash || llmReconfigHash === newLlmReconfigHash) { + _isConfiguring = false; + _isConfigurationDone = true; + return; + } + + // begin configuration + _isConfiguring = true; + setReconfigHash(llmReconfigHash); + + // find all vendors configured in the backend + // **NOTE**: doesn't reload pure frontend ones + const backendConfiguredVendors = findAllModelVendors() + .filter(vendor => vendor.hasBackendCapKey && backendCaps[vendor.hasBackendCapKey]); + + // List to keep track of the service IDs in order + const configuredServiceIds: DModelsServiceId[] = []; + + // Sequentially auto-configure each vendor + await backendConfiguredVendors.reduce(async (promiseChain, vendor) => { + return promiseChain + .then(async () => { + + // find the first service for this vendor + const { sources: modelsServices, addService } = llmsStoreState(); + let service: DModelsService; + const firstServiceForVendor = modelsServices.find(s => s.vId === vendor.id); + if (!firstServiceForVendor) { + // create and append the model service, assuming the backend configuration will be successful + service = createModelsServiceForVendor(vendor.id, modelsServices); + addService(service); + // re-find it now that it's added + service = llmsStoreState().sources.find(_s => _s.id === service.id)!; + } else + service = firstServiceForVendor; + + // keep track of the configured service IDs + configuredServiceIds.push(service.id); + + // auto-configure this service + await llmsUpdateModelsForServiceOrThrow(service.id, true); + }) + .catch(error => { + // catches errors and logs them, but does not stop the chain + console.error('Auto-configuration failed for vendor:', vendor.name, error); + }) + .then(() => { + // short delay between vendors + return new Promise(resolve => setTimeout(resolve, 50)); + }); + }, Promise.resolve()); + + // Re-rank the LLMs based on the order of configured services + llmsStoreActions().rerankLLMsByServices(configuredServiceIds); + + // end configuration + _isConfiguring = false; + _isConfigurationDone = true; + return true; +} \ No newline at end of file diff --git a/src/common/logic/store-logic-autoconf_vanilla.ts b/src/common/logic/store-logic-autoconf_vanilla.ts deleted file mode 100644 index ac00ee356..000000000 --- a/src/common/logic/store-logic-autoconf_vanilla.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { createStore as createVanillaStore } from 'zustand/vanilla'; -import { persist } from 'zustand/middleware'; - -import { createModelsServiceForVendor } from '~/modules/llms/vendors/vendor.helpers'; -import { findAllModelVendors } from '~/modules/llms/vendors/vendors.registry'; -import { getBackendCapabilities } from '~/modules/backend/store-backend-capabilities'; -import { llmsUpdateModelsForServiceOrThrow } from '~/modules/llms/llm.client'; - -import type { DModelsService, DModelsServiceId } from '~/common/stores/llms/modelsservice.types'; -import { llmsStoreActions, llmsStoreState } from '~/common/stores/llms/store-llms'; - - -interface AutoConfStore { - - // state - isConfiguring: boolean; - isConfigurationDone: boolean; - lastSeenBackendEnvHash: string; - - // actions - initiateConfiguration: () => Promise; - -} - - -const autoConfVanillaStore = createVanillaStore()(persist((_set, _get) => ({ - - // init state - isConfiguring: false, - isConfigurationDone: false, - lastSeenBackendEnvHash: '', - - - initiateConfiguration: async () => { - // Note: double-calling is only expected to happen in react strict mode - const { isConfiguring, isConfigurationDone, lastSeenBackendEnvHash } = _get(); - if (isConfiguring || isConfigurationDone) - return; - - // skip if no change is detected / no config needed - const backendCaps = getBackendCapabilities(); - const backendHash = backendCaps.llmConfigHash; - if (!backendHash || backendHash === lastSeenBackendEnvHash) - return _set({ isConfiguring: false, isConfigurationDone: true }); - - // begin configuration - _set({ isConfiguring: true, lastSeenBackendEnvHash: backendHash }); - - // find - let configurableVendors = findAllModelVendors() - .filter(vendor => vendor.hasBackendCapKey && backendCaps[vendor.hasBackendCapKey]); - - // List to keep track of the service IDs in order - const configuredServiceIds: DModelsServiceId[] = []; - - // Sequentially auto-configure each vendor - await configurableVendors.reduce(async (promiseChain, vendor) => { - return promiseChain - .then(async () => { - - // find the first service for this vendor - const { sources: modelsServices, addService } = llmsStoreState(); - let service: DModelsService; - const firstServiceForVendor = modelsServices.find(s => s.vId === vendor.id); - if (!firstServiceForVendor) { - // create and append the model service, assuming the backend configuration will be successful - service = createModelsServiceForVendor(vendor.id, modelsServices); - addService(service); - // re-find it now that it's added - service = llmsStoreState().sources.find(_s => _s.id === service.id)!; - } else - service = firstServiceForVendor; - - // keep track of the configured service IDs - configuredServiceIds.push(service.id); - - // auto-configure this service - await llmsUpdateModelsForServiceOrThrow(service.id, true); - }) - .catch(error => { - // catches errors and logs them, but does not stop the chain - console.error('Auto-configuration failed for vendor:', vendor.name, error); - }) - .then(() => { - // short delay between vendors - return new Promise(resolve => setTimeout(resolve, 50)); - }); - }, Promise.resolve()); - - // Re-rank the LLMs based on the order of configured services - llmsStoreActions().rerankLLMsByServices(configuredServiceIds); - - // end configuration - _set({ isConfiguring: false, isConfigurationDone: true }); - }, - -}), { - name: 'app-autoconf', - - // Pre-Saving: remove non-persisted properties - partialize: ({ lastSeenBackendEnvHash }) => ({ - lastSeenBackendEnvHash, - }), -})); - - -export function autoConfInitiateConfiguration() { - void autoConfVanillaStore.getState().initiateConfiguration(); -} diff --git a/src/common/logic/store-logic-sherpa.ts b/src/common/logic/store-logic-sherpa.ts index 4d5af1e60..df644facf 100644 --- a/src/common/logic/store-logic-sherpa.ts +++ b/src/common/logic/store-logic-sherpa.ts @@ -2,7 +2,8 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { useShallow } from 'zustand/react/shallow'; -import { incrementalNewsVersion } from '../../apps/news/news.version'; +import { Release } from '~/common/app.release'; +import { reconfigureBackendModels } from '~/common/logic/reconfigureBackendModels'; // Sherpa State: navigation thought the app, remembers the counters for progressive disclosure of complex features @@ -11,6 +12,7 @@ interface SherpaStore { usageCount: number; + lastLlmReconfigHash: string; lastSeenNewsVersion: number; chatComposerPrefill: string | null; // if not null, the composer will load this text at startup @@ -24,6 +26,7 @@ export const useLogicSherpaStore = create()( usageCount: 0, + lastLlmReconfigHash: '', lastSeenNewsVersion: 0, chatComposerPrefill: null, @@ -52,12 +55,22 @@ export function shallRedirectToNews() { } // if the news is outdated and the user has used the app a few times, show the news - const isNewsOutdated = (lastSeenNewsVersion || 0) < incrementalNewsVersion; + const isNewsOutdated = (lastSeenNewsVersion || 0) < Release.Monotonics.NewsVersion; return isNewsOutdated && usageCount >= 3; } export function markNewsAsSeen() { - useLogicSherpaStore.setState({ lastSeenNewsVersion: incrementalNewsVersion }); + useLogicSherpaStore.setState({ lastSeenNewsVersion: Release.Monotonics.NewsVersion }); +} + + +// Reconfgure Backend Models + +export async function sherpaReconfigureBackendModels() { + return reconfigureBackendModels( + useLogicSherpaStore.getState().lastLlmReconfigHash, + (hash: string) => useLogicSherpaStore.setState({ lastLlmReconfigHash: hash }), + ); } diff --git a/src/common/providers/ProviderBootstrapLogic.tsx b/src/common/providers/ProviderBootstrapLogic.tsx index eb53760ee..2ebf754ca 100644 --- a/src/common/providers/ProviderBootstrapLogic.tsx +++ b/src/common/providers/ProviderBootstrapLogic.tsx @@ -4,9 +4,8 @@ import { useRouter } from 'next/router'; import { gcAttachmentDBlobs } from '~/common/attachment-drafts/attachment.dblobs'; import { gcChatImageAssets } from '../../apps/chat/editors/image-generate'; -import { autoConfInitiateConfiguration } from '~/common/logic/store-logic-autoconf_vanilla'; import { estimatePersistentStorageOrThrow, requestPersistentStorage } from '~/common/util/storageUtils'; -import { markNewsAsSeen, shallRedirectToNews } from '~/common/logic/store-logic-sherpa'; +import { markNewsAsSeen, shallRedirectToNews, sherpaReconfigureBackendModels } from '~/common/logic/store-logic-sherpa'; import { navigateToNews, ROUTE_APP_CHAT } from '~/common/app.routes'; import { useNextLoadProgress } from '~/common/components/useNextLoadProgress'; @@ -27,7 +26,7 @@ export function ProviderBootstrapLogic(props: { children: React.ReactNode }) { // [autoconf] initiate the llm auto-configuration process if on the chat const doAutoConf = isOnChat && !doRedirectToNews; React.useEffect(() => { - doAutoConf && autoConfInitiateConfiguration(); + doAutoConf && void sherpaReconfigureBackendModels(); }, [doAutoConf]); // [gc] garbage collection(s) diff --git a/src/common/stores/llms/store-llms.ts b/src/common/stores/llms/store-llms.ts index 52c70af5c..32b735562 100644 --- a/src/common/stores/llms/store-llms.ts +++ b/src/common/stores/llms/store-llms.ts @@ -77,7 +77,11 @@ export const useModelsStore = create()(persist( ...llm, label: existing.label, // keep label hidden: existing.hidden, // keep hidden - options: { ...existing.options, ...llm.options }, // keep custom configurations, but overwrite as the new could have massively improved params + options: { + // keep custom configurations, but overwrite as the new could have massively improved params + ...existing.options, + ...llm.options, + }, }; }); } diff --git a/src/modules/backend/backend.router.ts b/src/modules/backend/backend.router.ts index 47eaba5e7..bd619007c 100644 --- a/src/modules/backend/backend.router.ts +++ b/src/modules/backend/backend.router.ts @@ -1,5 +1,7 @@ import { z } from 'zod'; +import { Release } from '~/common/app.release'; + import { createTRPCRouter, publicProcedure } from '~/server/api/trpc.server'; import { env } from '~/server/env.mjs'; import { fetchJsonOrTRPCThrow } from '~/server/api/trpc.router.fetchers'; @@ -21,13 +23,17 @@ function sdbmHash(str: string): string { } function generateLlmEnvConfigHash(env: Record): string { - return sdbmHash(Object.keys(env) - .filter(key => !!env[key]) // remove empty - .filter(key => key.includes('_API_')) // only include API keys - .sort() // ignore order - .map(key => `${key}=${env[key]}`) - .join(';'), - ); + const envAPIKeys = Object.keys(env) // get all env keys + .filter(key => !!env[key]) // minus the empty + .filter(key => key.includes('_API_')) // minus the non-API keys + .map(key => `${key}=${env[key]}`) // create key-value pairs + .sort(); // ignore order + const hashInputs = [ + Release.Monotonics.Aix.toString(), // triggers at every change (large downstream effect, know what you are doing) + Release.App.pl.toString(), // triggers when branch changes + ...envAPIKeys, // triggers when env keys change + ]; + return sdbmHash(hashInputs.join(';')); } @@ -44,10 +50,7 @@ export const backendRouter = createTRPCRouter({ .query(async ({ ctx }): Promise => { analyticsListCapabilities(ctx.hostName); return { - hasDB: (!!env.MDB_URI) || (!!env.POSTGRES_PRISMA_URL && !!env.POSTGRES_URL_NON_POOLING), - hasBrowsing: !!env.PUPPETEER_WSS_ENDPOINT, - hasGoogleCustomSearch: !!env.GOOGLE_CSE_ID && !!env.GOOGLE_CLOUD_API_KEY, - hasImagingProdia: !!env.PRODIA_API_KEY, + // llms hasLlmAnthropic: !!env.ANTHROPIC_API_KEY, hasLlmAzureOpenAI: !!env.AZURE_OPENAI_API_KEY && !!env.AZURE_OPENAI_API_ENDPOINT, hasLlmDeepseek: !!env.DEEPSEEK_API_KEY, @@ -62,15 +65,25 @@ export const backendRouter = createTRPCRouter({ hasLlmOpenRouter: !!env.OPENROUTER_API_KEY, hasLlmPerplexity: !!env.PERPLEXITY_API_KEY, hasLlmTogetherAI: !!env.TOGETHERAI_API_KEY, + // others + hasDB: (!!env.MDB_URI) || (!!env.POSTGRES_PRISMA_URL && !!env.POSTGRES_URL_NON_POOLING), + hasBrowsing: !!env.PUPPETEER_WSS_ENDPOINT, + hasGoogleCustomSearch: !!env.GOOGLE_CSE_ID && !!env.GOOGLE_CLOUD_API_KEY, + hasImagingProdia: !!env.PRODIA_API_KEY, hasVoiceElevenLabs: !!env.ELEVENLABS_API_KEY, - llmConfigHash: generateLlmEnvConfigHash(env), + // hashes + hashLlmReconfig: generateLlmEnvConfigHash(env), + // build data + build: Release.buildInfo('backend'), }; }), // The following are used for various OAuth integrations - /* Exchange the OpenrRouter 'code' (from PKCS) for an OpenRouter API Key */ + /** + * Exchange the OpenrRouter 'code' (from PKCS) for an OpenRouter API Key + */ exchangeOpenRouterKey: publicProcedure .input(z.object({ code: z.string() })) .query(async ({ input }) => { diff --git a/src/modules/backend/store-backend-capabilities.ts b/src/modules/backend/store-backend-capabilities.ts index 93d4b36be..beb6a9a96 100644 --- a/src/modules/backend/store-backend-capabilities.ts +++ b/src/modules/backend/store-backend-capabilities.ts @@ -7,10 +7,7 @@ import { useShallow } from 'zustand/react/shallow'; */ export interface BackendCapabilities { - hasDB: boolean; - hasBrowsing: boolean; - hasGoogleCustomSearch: boolean; - hasImagingProdia: boolean; + // llms hasLlmAnthropic: boolean; hasLlmAzureOpenAI: boolean; hasLlmDeepseek: boolean; @@ -25,23 +22,31 @@ export interface BackendCapabilities { hasLlmOpenRouter: boolean; hasLlmPerplexity: boolean; hasLlmTogetherAI: boolean; + // others + hasDB: boolean; + hasBrowsing: boolean; + hasGoogleCustomSearch: boolean; + hasImagingProdia: boolean; hasVoiceElevenLabs: boolean; - llmConfigHash: string; + // hashes + hashLlmReconfig: string; + // build data + build?: { + gitSha?: string; + pkgVersion?: string; + timestamp?: string; + }; } interface BackendStore extends BackendCapabilities { - loadedCapabilities: boolean; + _loadedCapabilities: boolean; setCapabilities: (capabilities: Partial) => void; } const useBackendCapabilitiesStore = create()( (set) => ({ - // capabilities - hasDB: false, - hasBrowsing: false, - hasGoogleCustomSearch: false, - hasImagingProdia: false, + // initial values hasLlmAnthropic: false, hasLlmAzureOpenAI: false, hasLlmDeepseek: false, @@ -56,14 +61,19 @@ const useBackendCapabilitiesStore = create()( hasLlmOpenRouter: false, hasLlmPerplexity: false, hasLlmTogetherAI: false, + hasDB: false, + hasBrowsing: false, + hasGoogleCustomSearch: false, + hasImagingProdia: false, hasVoiceElevenLabs: false, - llmConfigHash: '', + hashLlmReconfig: '', + build: undefined, + _loadedCapabilities: false, - loadedCapabilities: false, setCapabilities: (capabilities: Partial) => set({ - loadedCapabilities: true, ...capabilities, + _loadedCapabilities: true, }), }), @@ -71,7 +81,7 @@ const useBackendCapabilitiesStore = create()( export function useKnowledgeOfBackendCaps(): [boolean, (capabilities: Partial) => void] { - return useBackendCapabilitiesStore(useShallow(state => [state.loadedCapabilities, state.setCapabilities])); + return useBackendCapabilitiesStore(useShallow(state => [state._loadedCapabilities, state.setCapabilities])); } export function getBackendCapabilities(): BackendCapabilities {