mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 12:33:16 -05:00
The Memory add-on only declared three index files, two skills and a save setting; the host did the rest. Without the add-on a memory proposal worked but nothing was read back, and removing the add-on silently reset the save level. The host now loads the three indexes (user, agent, conversation) with the same caps and data wrapping, serves context_read, ships the two skills as built-in skills with the same text, and keeps the save level as its own per-person setting (ask, auto, never; default ask). The code sits behind a small MemoryProvider interface. The add-on stays installed and harmless: its index files and skills are ignored while the host serves memory, so nothing loads twice. A person who chose a level on its page keeps it until they choose in the host setting; auto still needs the add-on to run. A person who disabled the add-on and never chose keeps memory off. Saved agents still read notes only when their add-ons list names memory, and messaging chats still ask before saving. A person with no note and no add-on gets no extra block in the prompt. Assisted-by: Claude:claude-sonnet-5-5
117 lines
4.1 KiB
Go
117 lines
4.1 KiB
Go
package studio
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"sort"
|
|
|
|
"github.com/mudler/LocalAI/core/services/studio/contextfiles"
|
|
"github.com/mudler/LocalAI/pkg/addons/contract"
|
|
)
|
|
|
|
// Add-on context files. An add-on can declare small index files of the user's
|
|
// home (capabilities.contextFiles in its manifest). The host loads them into
|
|
// the prompt of a chat and serves their topics with context_read, with the same
|
|
// loader as any other source. The add-on is not called: it only names paths,
|
|
// and the host reads them as the user.
|
|
//
|
|
// A turn decides which add-ons contribute, as it does for their skills: a
|
|
// stopped add-on, one the chat switched off and one the chat's agent does not
|
|
// grant contribute none. The files stay where they are while the add-on is
|
|
// stopped or removed; they belong to the user.
|
|
//
|
|
// Distributed mode: the files are in the workspace store that every frontend
|
|
// reads, and which add-ons run is read per turn from the broker. Nothing is
|
|
// cached here.
|
|
|
|
// AddonContextSet is the context files one add-on declares, as the chats see
|
|
// them.
|
|
type AddonContextSet struct {
|
|
AddonID string
|
|
Files []contract.ContextFile
|
|
// Running is true when the add-on can serve now: installed, enabled and
|
|
// running.
|
|
Running bool
|
|
}
|
|
|
|
// AddonContextLister is implemented by a broker that knows the context files
|
|
// of the add-ons. It lists the installed add-ons that declare some and that
|
|
// the user can see, with whether each one runs. It is optional; a broker
|
|
// without it gives no add-on context files.
|
|
type AddonContextLister interface {
|
|
AddonContextFiles(ctx context.Context, user string) ([]AddonContextSet, error)
|
|
}
|
|
|
|
// sources turns the declaration of an add-on into loader sources. A file the
|
|
// loader would refuse is left out.
|
|
func (set AddonContextSet) sources() []contextfiles.Source {
|
|
var out []contextfiles.Source
|
|
for _, f := range set.Files {
|
|
src := contextfiles.Source{ID: set.AddonID, Scope: contextfiles.Scope(f.Scope), Path: f.Path, Cap: f.Cap}
|
|
if src.Validate() == nil {
|
|
out = append(out, src)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *Service) addonContextSets(ctx context.Context, user string) []AddonContextSet {
|
|
if s.d.AddonTools == nil {
|
|
return nil
|
|
}
|
|
lister, ok := s.d.AddonTools.(AddonContextLister)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
sets, err := lister.AddonContextFiles(ctx, user)
|
|
if err != nil {
|
|
s.log.Warn("studio: add-on context files unavailable", "user", user, "error", err)
|
|
return nil
|
|
}
|
|
sort.Slice(sets, func(i, j int) bool { return sets[i].AddonID < sets[j].AddonID })
|
|
return sets
|
|
}
|
|
|
|
// addonContextSources returns the sources the add-ons bring to a turn: those
|
|
// of the add-ons that run, that the chat did not switch off and, for a chat
|
|
// with an agent, that the agent grants.
|
|
func (s *Service) addonContextSources(ctx context.Context, user string, addonsOff []string, sc *scope) []contextfiles.Source {
|
|
return s.addonContextSourcesOf(ctx, user, s.addonContextSets(ctx, user), addonsOff, sc)
|
|
}
|
|
|
|
// addonContextSourcesOf is addonContextSources over sets already listed. The
|
|
// Memory add-on brings nothing: the host serves its notes (memory.go).
|
|
func (s *Service) addonContextSourcesOf(ctx context.Context, user string, sets []AddonContextSet, addonsOff []string, sc *scope) []contextfiles.Source {
|
|
if !s.d.Files.Available() {
|
|
return nil
|
|
}
|
|
var out []contextfiles.Source
|
|
gate, _ := s.d.AddonTools.(AddonGate)
|
|
for _, set := range sets {
|
|
if set.AddonID == MemoryAddonID {
|
|
continue
|
|
}
|
|
if !set.Running || slices.Contains(addonsOff, set.AddonID) || (sc != nil && !sc.addons[set.AddonID]) {
|
|
continue
|
|
}
|
|
if gate != nil && gate.Unavailable(ctx, user, set.AddonID, true) != "" {
|
|
continue
|
|
}
|
|
out = append(out, set.sources()...)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// allAddonContextSources returns the sources of every add-on the user can see,
|
|
// running or not: the files are the user's, and the Memory page lists them
|
|
// whether or not the add-on runs now.
|
|
func (s *Service) allAddonContextSources(ctx context.Context, user string) []contextfiles.Source {
|
|
out := s.memory.Sources()
|
|
for _, set := range s.addonContextSets(ctx, user) {
|
|
if set.AddonID != MemoryAddonID {
|
|
out = append(out, set.sources()...)
|
|
}
|
|
}
|
|
return out
|
|
}
|