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 or never. SaveMode string `json:"save_mode"` // 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 } // 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} 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) 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 or never", ErrInvalidSettings) } if (in.Enabled != nil || in.SaveMode != 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) } } 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() }