diff --git a/core/application/application.go b/core/application/application.go index 2efe929fb..5e96521d6 100644 --- a/core/application/application.go +++ b/core/application/application.go @@ -676,24 +676,29 @@ func (a *Application) start() error { a.agentJobService = agentJobService - // Initialize chat history store for the WebUI (issue #9432). Reuses the - // shared auth database so per-user chat history sits alongside agent - // configs and jobs in one place — mudler's review on #9902 called out - // the file-based store as inconsistent with the rest of LocalAI's - // per-user state. When auth is disabled the store stays nil and the - // React UI falls back to localStorage. - if !a.applicationConfig.DisableWebUI && a.authDB != nil { - store, err := chathistory.New(a.authDB) - if err != nil { - xlog.Warn("Chat history persistence disabled: failed to initialise store", "error", err) - } else { - a.chatHistoryStore = store - } - } - return nil } +// initChatHistoryStore initializes the chat history store for the WebUI +// (issue #9432). It reuses the shared auth database so per-user chat history +// sits alongside agent configs and jobs in one place — mudler's review on +// #9902 called out the file-based store as inconsistent with the rest of +// LocalAI's per-user state. When auth is disabled the store stays nil and the +// React UI falls back to localStorage. It must run after the auth database is +// opened: start() runs before that, so a store set up there was always nil +// and the conversation routes were never registered. +func (a *Application) initChatHistoryStore() { + if a.applicationConfig.DisableWebUI || a.authDB == nil { + return + } + store, err := chathistory.New(a.authDB) + if err != nil { + xlog.Warn("Chat history persistence disabled: failed to initialise store", "error", err) + return + } + a.chatHistoryStore = store +} + // agentPoolOptions builds the agent pool's dependencies. WorkQueue stays a nil // interface without distributed services, because the pool reads a non-nil // WorkQueue as distributed mode. diff --git a/core/application/chat_history_store_test.go b/core/application/chat_history_store_test.go new file mode 100644 index 000000000..feaf22d3b --- /dev/null +++ b/core/application/chat_history_store_test.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +//go:build auth + +package application + +import ( + "context" + "path/filepath" + + "github.com/mudler/LocalAI/core/config" + "github.com/mudler/LocalAI/pkg/system" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// The conversation routes are registered only when the store exists, so a +// store that New() never creates means /api/conversations answers 404 even +// with auth on. Built through New(), the same path the server takes. +var _ = Describe("chat history store at startup", func() { + newApp := func(opts ...config.AppOption) *Application { + ctx, cancel := context.WithCancel(context.Background()) + DeferCleanup(cancel) + state, err := system.GetSystemState(system.WithModelPath(GinkgoT().TempDir()), system.WithBackendPath(GinkgoT().TempDir())) + Expect(err).NotTo(HaveOccurred()) + base := []config.AppOption{config.WithContext(ctx), config.WithSystemState(state), config.DisableMetricsEndpoint, + config.WithDisableLocalAIAssistant(true), config.WithDisableStats(true), config.WithDataPath(GinkgoT().TempDir())} + app, err := New(append(base, opts...)...) + if app != nil { + DeferCleanup(func() { Expect(app.Shutdown()).To(Succeed()) }) + } + Expect(err).NotTo(HaveOccurred()) + return app + } + withAuth := func() []config.AppOption { + return []config.AppOption{config.WithAuthEnabled(true), + config.WithAuthDatabaseURL(filepath.Join(GinkgoT().TempDir(), "auth.db"))} + } + + It("is created when auth is enabled", func() { + Expect(newApp(withAuth()...).ChatHistoryStore()).NotTo(BeNil()) + }) + + It("stays nil with the WebUI disabled", func() { + Expect(newApp(append(withAuth(), config.DisableWebUI)...).ChatHistoryStore()).To(BeNil()) + }) + + It("stays nil without auth, so the UI keeps localStorage", func() { + Expect(newApp().ChatHistoryStore()).To(BeNil()) + }) +}) diff --git a/core/application/startup.go b/core/application/startup.go index b1be5f751..90c1f57ed 100644 --- a/core/application/startup.go +++ b/core/application/startup.go @@ -174,6 +174,7 @@ func New(opts ...config.AppOption) (*Application, error) { } }() } + application.initChatHistoryStore() // Initialize the OTel + Prometheus metric pipeline before any // counter is created. monitoring.NewLocalAIMetricsService calls