mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 12:33:16 -05:00
A person can now choose four save levels on the Memory tab: Save it and tell me, Ask me first (the default), Always confirm and Never. Always confirm is like ask, and nothing can loosen it: no agent or messaging setting overrides it, and the model cannot undo or forget a note by itself (revert_change is refused, and the person uses the Undo). One function, effectiveSaveLevel, combines the rules. The strictest wins, in the order auto, ask, always, never. The inputs are the level of the person, the rule of the saved agent, the messaging row and the chat. A table test covers every combination. A saved agent has an optional memory_save field: always or never. It can only be stricter than the person, so auto is not offered and the API refuses it. A write without the field keeps the stored rule, and use_mine clears it. It is stored by both stores and is part of an agent export and import. The agent form shows three cards under When it wants to save a note. The new row In messaging apps (messaging_save on GET and PUT /api/chats/memory) takes ask (the default), paired_private or never. With paired_private a note is saved at once only when the sender is paired and the chat is one-to-one, and it goes to the notes of that conversation, with an Undo. The limits stay: five notes a reply, the size limit, and a note that looks like a secret still waits. The row cannot loosen the level of the person when that is always or never. A setting that cannot be read reads as ask. The connector turn now carries whether the chat is one-to-one and whether the sender is paired, in the turn grant and in the worker JSON. Both read as false when unknown. The Telegram, Slack and Discord transports already set Direct; new specs pin it for private, group and supergroup chats, direct messages, channels, group conversations and threads. A note from a messaging chat goes to the conversation first. It reaches the notes of the agent or of the person only from a paired sender, only when the memory choice of the agent allows that scope, and only after that sender confirms it. Under Always confirm only the paired sender who asked can confirm. An agent whose memory is None still saves nothing. The script of the confirm-in-chat specs now proposes a note for the conversation, because a sender who is only listed cannot write to the notes of the person. Assisted-by: Claude:claude-sonnet-5-5
152 lines
5.5 KiB
Go
152 lines
5.5 KiB
Go
package studio
|
|
|
|
import (
|
|
"archive/zip"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/mudler/LocalAI/core/services/studio/contextfiles"
|
|
)
|
|
|
|
// ErrMemorySettings means the memory settings cannot be stored.
|
|
var ErrMemorySettings = errors.New("studio: the memory settings cannot be stored on this server")
|
|
|
|
// MemorySettings is the person's own memory choices, as the Memory tab shows
|
|
// them. The server keeps them with the person's other settings and every
|
|
// frontend reads the same answer.
|
|
type MemorySettings struct {
|
|
// Enabled is the switch in force: memory On or Off for every chat of the
|
|
// person. A chat, a messaging connection or an agent can only narrow it.
|
|
Enabled bool `json:"enabled"`
|
|
// EnabledSet is true once the person chose. Before that the switch is on,
|
|
// unless they had disabled the Memory add-on to stop memory.
|
|
EnabledSet bool `json:"enabled_set"`
|
|
// SaveMode is how a note is saved: ask, auto, always or never.
|
|
SaveMode string `json:"save_mode"`
|
|
// MessagingSave is how a note proposed in a messaging chat is saved: ask,
|
|
// paired_private or never. It never loosens always or never in SaveMode.
|
|
MessagingSave string `json:"messaging_save"`
|
|
// SaveModeSet is true once the person chose in this setting. Before that
|
|
// the value they gave on the page of the Memory add-on, if any, applies.
|
|
SaveModeSet bool `json:"save_mode_set"`
|
|
// Provider says what serves memory: "builtin".
|
|
Provider string `json:"provider"`
|
|
// Available is false when the server has no files: notes are files.
|
|
Available bool `json:"available"`
|
|
}
|
|
|
|
// MemoryUpdate is a change to the settings. A nil field is left alone.
|
|
type MemoryUpdate struct {
|
|
Enabled *bool
|
|
SaveMode *string
|
|
// MessagingSave is the messaging row: ask, paired_private or never.
|
|
MessagingSave *string
|
|
}
|
|
|
|
// MemorySettings reads the memory settings of a person.
|
|
func (s *Service) MemorySettings(ctx context.Context, user string) (MemorySettings, error) {
|
|
legacy := legacyMemoryOf(s.addonContextSets(ctx, user))
|
|
out := MemorySettings{Provider: s.memory.ID(), Available: s.d.Files.Available(), SaveMode: SaveAsk, MessagingSave: MessagingAsk}
|
|
out.Enabled = s.memoryUserOn(ctx, user, legacy)
|
|
if s.d.UserSetting != nil {
|
|
if v, ok, err := s.d.UserSetting(ctx, principal(user), HostSettingsID, MemoryEnabledKey); err == nil && ok && (v == MemoryOn || v == MemoryOff) {
|
|
out.EnabledSet = true
|
|
}
|
|
if v, ok, err := s.d.UserSetting(ctx, principal(user), HostSettingsID, MemorySaveKey); err == nil && ok && ValidSaveMode(v) {
|
|
out.SaveModeSet = true
|
|
}
|
|
}
|
|
out.SaveMode = s.changes.storedSaveMode(ctx, user)
|
|
out.MessagingSave = s.changes.storedMessagingSave(ctx, user)
|
|
return out, nil
|
|
}
|
|
|
|
// SetMemory stores the choices of a person. A value that is not one of the
|
|
// allowed ones is ErrInvalidSettings.
|
|
func (s *Service) SetMemory(ctx context.Context, user string, in MemoryUpdate) (MemorySettings, error) {
|
|
if in.SaveMode != nil && !ValidSaveMode(*in.SaveMode) {
|
|
return MemorySettings{}, fmt.Errorf("%w: save_mode must be ask, auto, always or never", ErrInvalidSettings)
|
|
}
|
|
if in.MessagingSave != nil && !ValidMessagingSave(*in.MessagingSave) {
|
|
return MemorySettings{}, fmt.Errorf("%w: messaging_save must be ask, paired_private or never", ErrInvalidSettings)
|
|
}
|
|
if (in.Enabled != nil || in.SaveMode != nil || in.MessagingSave != nil) && s.d.SetUserSetting == nil {
|
|
return MemorySettings{}, ErrMemorySettings
|
|
}
|
|
if in.Enabled != nil {
|
|
v := MemoryOff
|
|
if *in.Enabled {
|
|
v = MemoryOn
|
|
}
|
|
if err := s.d.SetUserSetting(ctx, principal(user), HostSettingsID, MemoryEnabledKey, v); err != nil {
|
|
return MemorySettings{}, fmt.Errorf("%w: %v", ErrMemorySettings, err)
|
|
}
|
|
}
|
|
if in.SaveMode != nil {
|
|
if err := s.d.SetUserSetting(ctx, principal(user), HostSettingsID, MemorySaveKey, *in.SaveMode); err != nil {
|
|
return MemorySettings{}, fmt.Errorf("%w: %v", ErrMemorySettings, err)
|
|
}
|
|
}
|
|
if in.MessagingSave != nil {
|
|
if err := s.d.SetUserSetting(ctx, principal(user), HostSettingsID, MemorySaveMessagingKey, *in.MessagingSave); err != nil {
|
|
return MemorySettings{}, fmt.Errorf("%w: %v", ErrMemorySettings, err)
|
|
}
|
|
}
|
|
return s.MemorySettings(ctx, user)
|
|
}
|
|
|
|
// ErrNoNotes means a scope has no notes to export.
|
|
var ErrNoNotes = errors.New("studio: there are no notes to export")
|
|
|
|
// ExportMemory writes the notes of a person as a zip file: the Markdown files
|
|
// of the memory folders, with their paths in the home. scope and key narrow it
|
|
// to one group, as the context files list names them (user; agent and the
|
|
// agent ID; channel and the folder name); empty exports all. It works while
|
|
// memory is off: the notes belong to the person.
|
|
func (s *Service) ExportMemory(ctx context.Context, user string, w io.Writer, scope, key string) (int, error) {
|
|
list, err := s.ListContextFiles(ctx, user)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
var paths []string
|
|
for _, g := range list.Groups {
|
|
if g.Source != MemoryAddonID {
|
|
continue
|
|
}
|
|
if scope != "" && (string(g.Scope) != scope || g.Key != key) {
|
|
continue
|
|
}
|
|
for _, f := range g.Files {
|
|
paths = append(paths, f.Path)
|
|
}
|
|
}
|
|
if len(paths) == 0 {
|
|
return 0, ErrNoNotes
|
|
}
|
|
zw := zip.NewWriter(w)
|
|
rd := homeReader{f: s.d.Files}
|
|
n := 0
|
|
for _, p := range paths {
|
|
data, err := rd.ReadFile(ctx, principal(user), p, 1<<20)
|
|
if err != nil {
|
|
if errors.Is(err, contextfiles.ErrNotExist) {
|
|
continue
|
|
}
|
|
return n, err
|
|
}
|
|
fw, err := zw.Create(strings.TrimPrefix(path.Clean(p), "/"))
|
|
if err != nil {
|
|
return n, err
|
|
}
|
|
if _, err := fw.Write(data); err != nil {
|
|
return n, err
|
|
}
|
|
n++
|
|
}
|
|
return n, zw.Close()
|
|
}
|