mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
Memory is the first provider slot. The person picks one provider for the
notes: the built-in one, Off, or an add-on that declares the slot. The
choice is kept per person with the other host settings.
Core stays generic. providerslots.go lists the providers of a slot,
stores and resolves the choice, and gates an add-on that declares a slot:
its context files, skills and role tools count only while it is the
provider picked, and the chat can narrow a slot off for itself. The
notes wire into it in memory.go: an adapter gives the add-on's files and
skills behind the MemoryProvider interface, the built-in provider stops
reading while another serves, and the On or Off switch and the choice
stay one answer.
If the add-on that was picked stops, is disabled or is removed, memory is
off with the code memory_provider_unavailable and the add-on is named.
It never falls back to the built-in provider, so a note is not read or
saved where the person did not choose. The choice stays stored.
Add GET /api/chats/providers, GET and PUT /api/chats/providers/{slot}.
Settings > Memory lists the providers from that answer with a generic
picker, shows the failure with the shared Notice and offers to use the
built-in provider. Document the facility for add-on authors, and test it
with a fake add-on that provides the slot.
Assisted-by: Claude:claude-sonnet-5-5 go
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DjrQyk8ovFXvy5zrjHX1BD
123 lines
4.3 KiB
Go
123 lines
4.3 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)
|
|
providers := s.providerGateFor(ctx, user, nil)
|
|
for _, set := range sets {
|
|
if set.AddonID == MemoryAddonID {
|
|
continue
|
|
}
|
|
// An add-on that provides a slot brings its files only while it is the
|
|
// provider the person picked.
|
|
if !providers.contributes(set.AddonID) {
|
|
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
|
|
}
|