mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
feat(memory): add a Memory On/Off switch and private chats
Memory now has one switch, On or Off, in the Memory tab of the account settings. Off means nothing from the notes is put in the prompt, the model has no memory skill, tool or note kind, nothing is proposed or saved (also by triggers and other runs that start by themselves), and the model is told in one line that memory is off. The notes stay and can still be read, exported and deleted. A chat can be private: the Memory row of its Tools menu is the memory switch group, stored with the chat. The narrowest level wins: chat, agent, person, server. A saved agent cannot widen the choice of the person. The tools list says which level is off (memory.code, blocked_by, fix); the chat shows a chip and one line, and the agent form shows the person's switch as a read-only line. New API: GET and PUT /api/chats/memory for the switch and the save level, and GET /api/chats/memory/export for a zip of the notes. The Memory tab gains the switch, the three save levels, Export and Forget everything. Assisted-by: Claude:claude-sonnet-5-5
This commit is contained in:
55 files changed
+2255
-80
No files matched your search
@@ -54,6 +54,11 @@ func (s *Service) describeGroups() {
|
||||
"A new trigger or a change waits on a card in the chat until you confirm it; pausing or resuming a trigger applies at once and you can undo it.",
|
||||
WhenOff: "The model cannot create or manage triggers. A run that a trigger or a connector started never has them.",
|
||||
},
|
||||
builtins.GroupMemory: {
|
||||
Title: "Memory",
|
||||
Description: "The model can read your notes before it answers and save new ones. Turn it off to make this a private chat: nothing is read from your notes or saved to them. Your other chats are not affected.",
|
||||
WhenOff: "This is a private chat. The model neither reads nor saves your notes.",
|
||||
},
|
||||
builtins.GroupChanges: {
|
||||
Title: "Suggested changes",
|
||||
Description: "The model can propose an edit to your notes, its standing rules, skills, pages and dashboard, and undo an edit you confirmed. " +
|
||||
|
||||
@@ -110,6 +110,9 @@ type ToolsPanel struct {
|
||||
// Fallbacks are the built-in tools an installed add-on replaces but no
|
||||
// add-on serves in this chat now.
|
||||
Fallbacks []ToolFallback
|
||||
// Memory says whether memory works in this chat and, when it does not,
|
||||
// which level turned it off and where to turn it on.
|
||||
Memory MemoryReason
|
||||
}
|
||||
|
||||
// Tools lists what a chat can use, with its switches.
|
||||
@@ -135,7 +138,7 @@ func (s *Service) toolsFor(ctx context.Context, user string, rec session.ChatRec
|
||||
return ToolsPanel{}, err
|
||||
}
|
||||
}
|
||||
p := ToolsPanel{Off: append([]string{}, rec.McpOff...), Builtins: s.builtinGroups(ctx, rec, shadowing)}
|
||||
p := ToolsPanel{Off: append([]string{}, rec.McpOff...), Builtins: s.builtinGroups(ctx, rec, shadowing), Memory: s.ChatMemory(ctx, rec)}
|
||||
if s.d.AddonTools != nil && (sc == nil || !sc.deleted) {
|
||||
p.Fallbacks, err = s.toolFallbacks(ctx, rec, shadowing, s.shadowersFor(ctx, principal(user)))
|
||||
if err != nil {
|
||||
|
||||
@@ -121,14 +121,19 @@ func memoryOff(code, by string) MemoryState { return MemoryState{Code: code, By:
|
||||
// is the Memory add-on, for the person who never chose.
|
||||
func (s *Service) memoryState(ctx context.Context, rec session.ChatRecord, legacy legacyMemory) MemoryState {
|
||||
switch {
|
||||
case !s.d.Files.Available():
|
||||
return memoryOff(MemoryOffFiles, "server")
|
||||
case slices.Contains(rec.BuiltinsOff, builtins.GroupMemory):
|
||||
// A private chat.
|
||||
return memoryOff(MemoryOffChat, "chat")
|
||||
case slices.Contains(rec.AddonsOff, MemoryAddonID):
|
||||
// The chat switched the Memory add-on off: the same switch, in the
|
||||
// words it had before memory was built in.
|
||||
return memoryOff(MemoryOffChat, "chat")
|
||||
case slices.Contains(s.agentOff(ctx, rec), builtins.GroupMemory):
|
||||
return memoryOff(MemoryOffAgent, "agent")
|
||||
case !s.memoryUserOn(ctx, rec.Principal, legacy):
|
||||
return memoryOff(MemoryOffUser, "user")
|
||||
case !s.d.Files.Available():
|
||||
return memoryOff(MemoryOffFiles, "server")
|
||||
}
|
||||
return MemoryState{On: true}
|
||||
}
|
||||
@@ -203,3 +208,90 @@ func (s *Service) memoryWritable(ctx context.Context, c builtins.Chat) bool {
|
||||
legacy := legacyMemoryOf(s.addonContextSets(ctx, c.Actor.Principal))
|
||||
return s.memoryState(ctx, rec, legacy).On && !slices.Contains(c.GroupsOff, builtins.GroupMemory)
|
||||
}
|
||||
|
||||
// memoryOffLine is the one line the model reads while memory is off for a
|
||||
// chat: it hears that nothing is read or saved and what to say when asked.
|
||||
func memoryOffLine(st MemoryState) string {
|
||||
where := "in the user's settings"
|
||||
switch st.By {
|
||||
case "chat":
|
||||
where = "for this chat, which is private"
|
||||
case "agent":
|
||||
where = "for this agent"
|
||||
}
|
||||
return "Memory is off " + where + ", so notes are neither read nor saved. If asked to remember something, say that memory is off and that the user can turn it on again; do not try to save it any other way."
|
||||
}
|
||||
|
||||
// MemoryReason is a memory state worded for a person, with where to fix it.
|
||||
type MemoryReason struct {
|
||||
On bool `json:"on"`
|
||||
// Code is a stable name of the level that is off: memory_off_chat,
|
||||
// memory_off_agent, memory_off_user or memory_off_files. Empty when on.
|
||||
Code string `json:"code,omitempty"`
|
||||
// By is who holds that switch: chat, agent, user or server.
|
||||
By string `json:"blocked_by,omitempty"`
|
||||
// Reason is an English sentence for logs and tests.
|
||||
Reason string `json:"reason,omitempty"`
|
||||
// Fix says where to turn it on again.
|
||||
Fix *Fix `json:"fix,omitempty"`
|
||||
}
|
||||
|
||||
// The fixes of a memory switch that is off.
|
||||
const (
|
||||
// FixOpenMemorySettings points at the Memory tab of the account settings.
|
||||
FixOpenMemorySettings = "open_memory_settings"
|
||||
// FixOpenToolsMenu points at the Tools menu of the chat.
|
||||
FixOpenToolsMenu = "open_tools_menu"
|
||||
)
|
||||
|
||||
// memoryReason words a state.
|
||||
func memoryReason(st MemoryState, rec session.ChatRecord) MemoryReason {
|
||||
out := MemoryReason{On: st.On, Code: st.Code, By: st.By}
|
||||
switch st.Code {
|
||||
case MemoryOffChat:
|
||||
out.Reason = "Memory is off in this chat: it is a private chat."
|
||||
out.Fix = &Fix{Kind: FixOpenToolsMenu, Target: "memory"}
|
||||
case MemoryOffAgent:
|
||||
out.Reason = "Memory is off for the agent of this chat."
|
||||
out.Fix = &Fix{Kind: FixOpenAgent, Target: rec.AgentID, Field: "tools:" + builtins.GroupMemory}
|
||||
case MemoryOffUser:
|
||||
out.Reason = "Memory is off in your settings."
|
||||
out.Fix = &Fix{Kind: FixOpenMemorySettings, Target: "memory"}
|
||||
case MemoryOffFiles:
|
||||
out.Reason = "Memory needs files, and files are off on this server."
|
||||
out.Fix = &Fix{Kind: FixOpenServerSetting, Target: "files"}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ChatMemory says whether memory works in a chat and, when it does not, which
|
||||
// level turned it off.
|
||||
func (s *Service) ChatMemory(ctx context.Context, rec session.ChatRecord) MemoryReason {
|
||||
legacy := legacyMemoryOf(s.addonContextSets(ctx, rec.Principal))
|
||||
return memoryReason(s.memoryState(ctx, rec, legacy), rec)
|
||||
}
|
||||
|
||||
// registerMemoryGroup puts memory in the Tools menu as one switch group. The
|
||||
// group has no tool of its own to build: the host reads the switch when it
|
||||
// builds the prompt, so a private chat has no notes in it and no context_read.
|
||||
// Build only says whether the switch can work now, and why not.
|
||||
func (s *Service) registerMemoryGroup() error {
|
||||
if !s.d.Files.Available() {
|
||||
return nil // notes are files: without them there is no switch to show
|
||||
}
|
||||
return s.builtins.Register(builtins.Tool{
|
||||
ID: contextfiles.ToolName, Group: builtins.GroupMemory, Label: "Memory", External: true, Unlisted: true,
|
||||
Description: "Read the notes of the user's memory.",
|
||||
Build: func(ctx context.Context, c builtins.Chat) (session.Tool, bool, string) {
|
||||
switch {
|
||||
case !s.d.Files.Available():
|
||||
return session.Tool{}, false, "memory needs files, and files are off on this server"
|
||||
case c.SubAgent:
|
||||
return session.Tool{}, false, "a sub-agent follows the memory of its parent chat"
|
||||
case !s.memoryUserOn(ctx, c.Actor.Principal, legacyMemoryOf(s.addonContextSets(ctx, c.Actor.Principal))):
|
||||
return session.Tool{}, false, "memory is off in your settings"
|
||||
}
|
||||
return session.Tool{}, true, ""
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package studio
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
@@ -141,6 +143,12 @@ var _ = Describe("memory served by the host", func() {
|
||||
ag, _ := agents.NewJSON("")
|
||||
br = &memBroker{}
|
||||
svc, err = New(Deps{Store: st, Provider: provider.New(), StateDir: GinkgoT().TempDir(), Files: f, Agents: ag, AddonTools: br,
|
||||
SetUserSetting: func(_ context.Context, _, addon, key, value string) error {
|
||||
smu.Lock()
|
||||
defer smu.Unlock()
|
||||
stored[addon+"/"+key] = value
|
||||
return nil
|
||||
},
|
||||
UserSetting: func(_ context.Context, _, addon, key string) (string, bool, error) {
|
||||
smu.Lock()
|
||||
defer smu.Unlock()
|
||||
@@ -268,4 +276,173 @@ var _ = Describe("memory served by the host", func() {
|
||||
Expect(mode()).To(Equal(SaveNever), "an unknown value is not a choice")
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Off and private chats", func() {
|
||||
hasTool := func(tt session.TurnTools, name string) bool {
|
||||
for _, t := range tt.Tools {
|
||||
if t.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
offLine := "Memory is off"
|
||||
expectOff := func(tt session.TurnTools, why string) {
|
||||
GinkgoHelper()
|
||||
Expect(tt.Prompt).NotTo(ContainSubstring("lives in Rome"), why)
|
||||
Expect(tt.Prompt).NotTo(ContainSubstring(contextfiles.Header), why)
|
||||
Expect(tt.Prompt).To(ContainSubstring(offLine), why)
|
||||
Expect(strings.Count(tt.Prompt, offLine)).To(Equal(1), why)
|
||||
Expect(hasTool(tt, contextfiles.ToolName)).To(BeFalse(), why)
|
||||
for _, t := range tt.Tools {
|
||||
if t.Name == "propose_change" {
|
||||
// The other kinds of change stay; the memory kind goes.
|
||||
Expect(string(t.Schema)).NotTo(ContainSubstring(`"memory"`), why+": no note is proposed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
It("offers notes and the save tool while memory is on", func() {
|
||||
tt := turn(CreateInput{Model: "m"})
|
||||
Expect(tt.Prompt).To(ContainSubstring("lives in Rome"))
|
||||
Expect(tt.Prompt).NotTo(ContainSubstring(offLine))
|
||||
var schema string
|
||||
for _, t := range tt.Tools {
|
||||
if t.Name == "propose_change" {
|
||||
schema = string(t.Schema)
|
||||
}
|
||||
}
|
||||
Expect(schema).To(ContainSubstring(`"memory"`))
|
||||
})
|
||||
|
||||
It("turns everything off for a private chat, and only that chat", func() {
|
||||
expectOff(turn(CreateInput{Model: "m", Switches: Switches{BuiltinsOff: []string{"memory"}}}), "private chat")
|
||||
Expect(turn(CreateInput{Model: "m"}).Prompt).To(ContainSubstring("lives in Rome"))
|
||||
})
|
||||
|
||||
It("turns everything off for a person who switched memory off, whatever else says", func() {
|
||||
setting(HostSettingsID, MemoryEnabledKey, MemoryOff)
|
||||
expectOff(turn(CreateInput{Model: "m"}), "user off")
|
||||
agent, err := svc.SaveAgentWithFolder(ctx, "alice", agents.Agent{Name: "Granted", Model: "m", Addons: []string{"memory"},
|
||||
Builtins: []string{contract.CapMemoryWrite}}, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
expectOff(turn(CreateInput{Model: "m", AgentID: agent.ID}), "a saved agent cannot widen the choice of the person")
|
||||
// Nothing in the chat can widen it either.
|
||||
expectOff(turn(CreateInput{Model: "m", Switches: Switches{AddonsOff: nil, BuiltinsOff: nil}}), "chat switches only narrow")
|
||||
})
|
||||
|
||||
It("honours it in a run that started by itself", func() {
|
||||
setting(HostSettingsID, MemoryEnabledKey, MemoryOff)
|
||||
rec, err := svc.CreateAs(ctx, "alice", CreateInput{Model: "m"}, Origin{Kind: session.OriginTrigger, ID: "t1"})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
x, err := svc.ChatAccess(ctx, "alice", rec.ID)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
tt, err := svc.chatTools(ctx, x)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(tt.Prompt).NotTo(ContainSubstring("lives in Rome"))
|
||||
Expect(tt.Prompt).To(ContainSubstring(offLine))
|
||||
})
|
||||
|
||||
It("honours the memory switch of the agent", func() {
|
||||
agent, err := svc.SaveAgentWithFolder(ctx, "alice", agents.Agent{Name: "Quiet", Model: "m", Addons: []string{"memory"},
|
||||
Builtins: []string{contract.CapMemoryWrite}, BuiltinsOff: []string{"memory"}}, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
tt := turn(CreateInput{Model: "m", AgentID: agent.ID})
|
||||
Expect(tt.Prompt).NotTo(ContainSubstring("lives in Rome"))
|
||||
Expect(tt.Prompt).To(ContainSubstring("for this agent"))
|
||||
})
|
||||
|
||||
It("takes the two memory skills away while memory is off", func() {
|
||||
rec, err := svc.CreateAs(ctx, "alice", CreateInput{Model: "m", Switches: Switches{BuiltinsOff: []string{"memory"}}}, Origin{})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
list, _, err := svc.Skills(ctx, "alice", rec.ID)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
seen := 0
|
||||
for _, sk := range list {
|
||||
if sk.Name == "memory" || sk.Name == "memory-check-own-files" {
|
||||
seen++
|
||||
Expect(sk.Off).To(BeTrue(), sk.Name)
|
||||
}
|
||||
}
|
||||
Expect(seen).To(Equal(2))
|
||||
})
|
||||
|
||||
It("says which level is off, and where to turn it on", func() {
|
||||
rec, err := svc.CreateAs(ctx, "alice", CreateInput{Model: "m", Switches: Switches{BuiltinsOff: []string{"memory"}}}, Origin{})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
r := svc.ChatMemory(ctx, rec)
|
||||
Expect(r.On).To(BeFalse())
|
||||
Expect(r.Code).To(Equal(MemoryOffChat))
|
||||
Expect(r.By).To(Equal("chat"))
|
||||
Expect(r.Fix).To(Equal(&Fix{Kind: FixOpenToolsMenu, Target: "memory"}))
|
||||
setting(HostSettingsID, MemoryEnabledKey, MemoryOff)
|
||||
plain, err := svc.CreateAs(ctx, "alice", CreateInput{Model: "m"}, Origin{})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
r = svc.ChatMemory(ctx, plain)
|
||||
Expect(r.Code).To(Equal(MemoryOffUser))
|
||||
Expect(r.Fix.Kind).To(Equal(FixOpenMemorySettings))
|
||||
Expect(svc.ChatMemory(ctx, rec).Code).To(Equal(MemoryOffChat), "the narrowest level is the one named")
|
||||
})
|
||||
|
||||
It("lists the memory group in the Tools menu, off for a private chat and unusable when the person switched it off", func() {
|
||||
panel, err := svc.DraftTools(ctx, "alice", "m", "")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
var got *BuiltinGroup
|
||||
for i := range panel.Builtins {
|
||||
if panel.Builtins[i].Group == "memory" {
|
||||
got = &panel.Builtins[i]
|
||||
}
|
||||
}
|
||||
Expect(got).NotTo(BeNil())
|
||||
Expect(got.Off).To(BeFalse())
|
||||
Expect(got.Usable).To(BeTrue())
|
||||
Expect(got.Title).To(Equal("Memory"))
|
||||
setting(HostSettingsID, MemoryEnabledKey, MemoryOff)
|
||||
panel, err = svc.DraftTools(ctx, "alice", "m", "")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
for _, g := range panel.Builtins {
|
||||
if g.Group == "memory" {
|
||||
Expect(g.Usable).To(BeFalse())
|
||||
Expect(g.Reason).To(ContainSubstring("off in your settings"))
|
||||
}
|
||||
}
|
||||
Expect(panel.Memory.Code).To(Equal(MemoryOffUser))
|
||||
})
|
||||
|
||||
It("keeps the notes viewable and exportable while memory is off", func() {
|
||||
setting(HostSettingsID, MemoryEnabledKey, MemoryOff)
|
||||
list, err := svc.ListContextFiles(ctx, "alice")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(list.Groups).NotTo(BeEmpty())
|
||||
var buf bytes.Buffer
|
||||
n, err := svc.ExportMemory(ctx, "alice", &buf, "", "")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(n).To(Equal(2))
|
||||
zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
var names []string
|
||||
for _, f := range zr.File {
|
||||
names = append(names, f.Name)
|
||||
}
|
||||
Expect(names).To(ConsistOf("Memory/index.md", "Memory/home.md"))
|
||||
_, err = svc.ExportMemory(ctx, "alice", &bytes.Buffer{}, "agent", "nobody")
|
||||
Expect(err).To(MatchError(ErrNoNotes))
|
||||
})
|
||||
|
||||
It("stores the switch and the save level with the person's settings", func() {
|
||||
m, err := svc.MemorySettings(ctx, "alice")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(m).To(Equal(MemorySettings{Enabled: true, SaveMode: SaveAsk, Provider: "builtin", Available: true}))
|
||||
off, auto := false, SaveAuto
|
||||
m, err = svc.SetMemory(ctx, "alice", MemoryUpdate{Enabled: &off, SaveMode: &auto})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(m.Enabled).To(BeFalse())
|
||||
Expect(m.EnabledSet).To(BeTrue())
|
||||
Expect(m.SaveMode).To(Equal(SaveAuto))
|
||||
Expect(m.SaveModeSet).To(BeTrue())
|
||||
bad := "always"
|
||||
_, err = svc.SetMemory(ctx, "alice", MemoryUpdate{SaveMode: &bad})
|
||||
Expect(err).To(MatchError(ErrInvalidSettings))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,137 @@
|
||||
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()
|
||||
}
|
||||
@@ -63,6 +63,8 @@ const (
|
||||
)
|
||||
|
||||
var allowed = []entry{
|
||||
{ui + "hooks/useToolGroups.js", "memory", "the switch group of the built-in memory"},
|
||||
{ui + "utils/memoryState.js", "memory", "the switch group of the built-in memory"},
|
||||
// Memory is built into the host. The add-on of that name is the alias of
|
||||
// the built-in: its index files and skills are the host's now, and the
|
||||
// host ignores its copies.
|
||||
|
||||
@@ -472,6 +472,9 @@ func New(d Deps) (*Service, error) {
|
||||
if err := s.registerToolGroups(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.registerMemoryGroup(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.memory = builtinMemory{}
|
||||
s.ctxfiles = contextfiles.NewLoader(contextfiles.NewRegistry(), homeReader{f: d.Files}, log)
|
||||
s.shared = sharedfolders.New(sharedfolders.Options{Files: d.Files, Consents: d.PageConsents, Audit: d.SharedFolderAudit, Users: s.folderUsers()})
|
||||
|
||||
@@ -152,6 +152,11 @@ func (s *Service) chatTools(ctx context.Context, x session.Access) (session.Turn
|
||||
}
|
||||
notes, notesKey = s.contextTurnFor(ctx, rec, tools, extra, !(legacy.Installed && legacy.Running))
|
||||
tools = append(tools, s.contextTools(rec, tools, extra)...)
|
||||
if st := s.memoryState(ctx, rec, legacy); !st.On && st.Code != MemoryOffFiles {
|
||||
// Off: the model hears it in one line, and has no note and no tool.
|
||||
line := memoryOffLine(st)
|
||||
notes, notesKey = joinBlocks(notes, line), notesKey+line
|
||||
}
|
||||
}
|
||||
if lead := joinBlocks(persona, notes); lead != "" {
|
||||
prompt = strings.TrimSpace(lead + "\n\n" + prompt)
|
||||
|
||||
@@ -238,7 +238,7 @@ var _ = Describe("the one registry of built-in tools", func() {
|
||||
if t.ID == "load_skill" || t.ID == "goal_done" || t.ID == "goal_blocked" || t.ID == studio.SendMessageName {
|
||||
Expect(t.Group).To(BeEmpty(), t.ID)
|
||||
} else if t.External {
|
||||
Expect(t.Group).To(BeElementOf(builtins.GroupFiles, builtins.GroupMedia, builtins.GroupCollections, builtins.GroupSubAgents), t.ID)
|
||||
Expect(t.Group).To(BeElementOf(builtins.GroupFiles, builtins.GroupMedia, builtins.GroupCollections, builtins.GroupSubAgents, builtins.GroupMemory), t.ID)
|
||||
} else {
|
||||
Expect(t.Group).To(BeElementOf(builtins.GroupTriggers, builtins.GroupChanges), t.ID)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user