feat(memories): add action to handle simple memory as flat files (#252)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
committed by
GitHub
parent
ba8ea0117e
commit
4cf52ec698
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/action"
|
||||
"github.com/mudler/LocalAGI/core/state"
|
||||
@@ -50,6 +51,9 @@ const (
|
||||
ActionSetReminder = "set_reminder"
|
||||
ActionListReminders = "list_reminders"
|
||||
ActionRemoveReminder = "remove_reminder"
|
||||
ActionAddToMemory = "add_to_memory"
|
||||
ActionListMemory = "list_memory"
|
||||
ActionRemoveFromMemory = "remove_from_memory"
|
||||
)
|
||||
|
||||
var AvailableActions = []string{
|
||||
@@ -87,12 +91,16 @@ var AvailableActions = []string{
|
||||
ActionSetReminder,
|
||||
ActionListReminders,
|
||||
ActionRemoveReminder,
|
||||
ActionAddToMemory,
|
||||
ActionListMemory,
|
||||
ActionRemoveFromMemory,
|
||||
}
|
||||
|
||||
const (
|
||||
ActionConfigBrowserAgentRunner = "browser-agent-runner-base-url"
|
||||
ActionConfigDeepResearchRunner = "deep-research-runner-base-url"
|
||||
ActionConfigSSHBoxURL = "sshbox-url"
|
||||
ActionConfigStateDir = "state-dir"
|
||||
)
|
||||
|
||||
func Actions(actionsConfigs map[string]string) func(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
|
||||
@@ -130,6 +138,18 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
||||
config = map[string]string{}
|
||||
}
|
||||
|
||||
// Compose memory file path based on stateDir and agentName, using a subdirectory
|
||||
memoryFilePath := "memory.json"
|
||||
if actionsConfigs != nil {
|
||||
if stateDir, ok := actionsConfigs[ActionConfigStateDir]; ok && stateDir != "" {
|
||||
memoryDir := fmt.Sprintf("%s/memory", stateDir)
|
||||
_ = os.MkdirAll(memoryDir, 0755) // ensure the directory exists
|
||||
memoryFilePath = fmt.Sprintf("%s/%s.json", memoryDir, agentName)
|
||||
} else {
|
||||
memoryFilePath = fmt.Sprintf("%s.memory.json", agentName)
|
||||
}
|
||||
}
|
||||
|
||||
switch name {
|
||||
case ActionCustom:
|
||||
a, err = action.NewCustom(config, "")
|
||||
@@ -199,6 +219,12 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
||||
a = action.NewListReminders()
|
||||
case ActionRemoveReminder:
|
||||
a = action.NewRemoveReminder()
|
||||
case ActionAddToMemory:
|
||||
a, _, _ = actions.NewMemoryActions(memoryFilePath, config)
|
||||
case ActionListMemory:
|
||||
_, a, _ = actions.NewMemoryActions(memoryFilePath, config)
|
||||
case ActionRemoveFromMemory:
|
||||
_, _, a = actions.NewMemoryActions(memoryFilePath, config)
|
||||
default:
|
||||
xlog.Error("Action not found", "name", name)
|
||||
return nil, fmt.Errorf("Action not found")
|
||||
@@ -233,6 +259,21 @@ func ActionsConfigMeta() []config.FieldGroup {
|
||||
Label: "Generate Image",
|
||||
Fields: actions.GenImageConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "add_to_memory",
|
||||
Label: "Add to Memory",
|
||||
Fields: actions.AddToMemoryConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "list_memory",
|
||||
Label: "List Memory",
|
||||
Fields: actions.ListMemoryConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "remove_from_memory",
|
||||
Label: "Remove from Memory",
|
||||
Fields: actions.RemoveFromMemoryConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-labeler",
|
||||
Label: "GitHub Issue Labeler",
|
||||
|
||||
284
services/actions/memory.go
Normal file
284
services/actions/memory.go
Normal file
@@ -0,0 +1,284 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"io"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
// Remove global const and mutex, and add them as fields to a struct
|
||||
|
||||
type MemoryActions struct {
|
||||
filePath string
|
||||
customName string
|
||||
customDescription string
|
||||
}
|
||||
|
||||
type AddToMemoryAction struct{ *MemoryActions }
|
||||
type ListMemoryAction struct{ *MemoryActions }
|
||||
type RemoveFromMemoryAction struct{ *MemoryActions }
|
||||
|
||||
// NewMemoryActions returns the three actions, using the provided filePath and config
|
||||
func NewMemoryActions(filePath string, config map[string]string) (*AddToMemoryAction, *ListMemoryAction, *RemoveFromMemoryAction) {
|
||||
ma := &MemoryActions{filePath: filePath}
|
||||
if config != nil {
|
||||
ma.customName = config["custom_name"]
|
||||
ma.customDescription = config["custom_description"]
|
||||
}
|
||||
return &AddToMemoryAction{ma}, &ListMemoryAction{ma}, &RemoveFromMemoryAction{ma}
|
||||
}
|
||||
|
||||
type addToMemoryParams struct {
|
||||
Item string `json:"item"`
|
||||
}
|
||||
|
||||
type removeFromMemoryParams struct {
|
||||
Index *int `json:"index,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (m *MemoryActions) readMemory() ([]string, error) {
|
||||
f, err := os.Open(m.filePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
var items []string
|
||||
if err := json.NewDecoder(f).Decode(&items); err != nil {
|
||||
if err == io.EOF {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (m *MemoryActions) writeMemory(items []string) error {
|
||||
f, err := os.Create(m.filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return json.NewEncoder(f).Encode(items)
|
||||
}
|
||||
|
||||
func (a *AddToMemoryAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
var req addToMemoryParams
|
||||
if err := params.Unmarshal(&req); err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
if req.Item == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("item cannot be empty")
|
||||
}
|
||||
items, err := a.readMemory()
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
items = append(items, req.Item)
|
||||
if err := a.writeMemory(items); err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Added item to memory: %s", req.Item),
|
||||
Metadata: map[string]any{"item": req.Item, "count": len(items)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *ListMemoryAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
items, err := a.readMemory()
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
outputResult := "Number of items in memory: " + strconv.Itoa(len(items)) + "\n"
|
||||
for i, item := range items {
|
||||
outputResult += fmt.Sprintf("%d) %s\n", i, item)
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: outputResult,
|
||||
Metadata: map[string]any{"items": items},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *RemoveFromMemoryAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
var req removeFromMemoryParams
|
||||
if err := params.Unmarshal(&req); err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
items, err := a.readMemory()
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
var removed string
|
||||
if req.Index != nil {
|
||||
idx := *req.Index
|
||||
if idx < 0 || idx >= len(items) {
|
||||
return types.ActionResult{}, fmt.Errorf("index out of range")
|
||||
}
|
||||
removed = items[idx]
|
||||
items = append(items[:idx], items[idx+1:]...)
|
||||
} else if req.Value != "" {
|
||||
found := false
|
||||
for i, v := range items {
|
||||
if v == req.Value {
|
||||
removed = v
|
||||
items = append(items[:i], items[i+1:]...)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return types.ActionResult{}, fmt.Errorf("value not found in memory")
|
||||
}
|
||||
} else {
|
||||
return types.ActionResult{}, fmt.Errorf("must provide index or value to remove")
|
||||
}
|
||||
if err := a.writeMemory(items); err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Removed item from memory: %s", removed),
|
||||
Metadata: map[string]any{"removed": removed, "count": len(items)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *AddToMemoryAction) Definition() types.ActionDefinition {
|
||||
name := "add_to_memory"
|
||||
description := "Add a string item to memory (stored in a JSON file)."
|
||||
if a.customName != "" {
|
||||
name = a.customName
|
||||
}
|
||||
if a.customDescription != "" {
|
||||
description = a.customDescription
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(name),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"item": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The string item to add to memory.",
|
||||
},
|
||||
},
|
||||
Required: []string{"item"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ListMemoryAction) Definition() types.ActionDefinition {
|
||||
name := "list_memory"
|
||||
description := "List all items currently stored in memory."
|
||||
if a.customName != "" {
|
||||
name = a.customName
|
||||
}
|
||||
if a.customDescription != "" {
|
||||
description = a.customDescription
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(name),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{},
|
||||
Required: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *RemoveFromMemoryAction) Definition() types.ActionDefinition {
|
||||
name := "remove_from_memory"
|
||||
description := "Remove an item from memory by index or value."
|
||||
if a.customName != "" {
|
||||
name = a.customName
|
||||
}
|
||||
if a.customDescription != "" {
|
||||
description = a.customDescription
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(name),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"index": {
|
||||
Type: jsonschema.Integer,
|
||||
Description: "The index of the item to remove (optional, 0-based)",
|
||||
},
|
||||
"value": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The value of the item to remove (optional)",
|
||||
},
|
||||
},
|
||||
Required: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AddToMemoryAction) Plannable() bool { return true }
|
||||
func (a *ListMemoryAction) Plannable() bool { return true }
|
||||
func (a *RemoveFromMemoryAction) Plannable() bool { return true }
|
||||
|
||||
// AddToMemoryConfigMeta returns the metadata for AddToMemory action configuration fields
|
||||
func AddToMemoryConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "custom_name",
|
||||
Label: "Custom Name",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom name for the action (optional, defaults to 'add_to_memory')",
|
||||
},
|
||||
{
|
||||
Name: "custom_description",
|
||||
Label: "Custom Description",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom description for the action (optional, defaults to 'Add a string item to memory (stored in a JSON file).')",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ListMemoryConfigMeta returns the metadata for ListMemory action configuration fields
|
||||
func ListMemoryConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "custom_name",
|
||||
Label: "Custom Name",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom name for the action (optional, defaults to 'list_memory')",
|
||||
},
|
||||
{
|
||||
Name: "custom_description",
|
||||
Label: "Custom Description",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom description for the action (optional, defaults to 'List all items currently stored in memory.')",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveFromMemoryConfigMeta returns the metadata for RemoveFromMemory action configuration fields
|
||||
func RemoveFromMemoryConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "custom_name",
|
||||
Label: "Custom Name",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom name for the action (optional, defaults to 'remove_from_memory')",
|
||||
},
|
||||
{
|
||||
Name: "custom_description",
|
||||
Label: "Custom Description",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom description for the action (optional, defaults to 'Remove an item from memory by index or value.')",
|
||||
},
|
||||
}
|
||||
}
|
||||
83
services/actions/memory_test.go
Normal file
83
services/actions/memory_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package actions_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/services/actions"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("MemoryActions", func() {
|
||||
var (
|
||||
tmpFile string
|
||||
aAdd *actions.AddToMemoryAction
|
||||
aList *actions.ListMemoryAction
|
||||
aRemove *actions.RemoveFromMemoryAction
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
f, err := os.CreateTemp("", "memory_test_*.json")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
tmpFile = f.Name()
|
||||
f.Close()
|
||||
aAdd, aList, aRemove = actions.NewMemoryActions(tmpFile, map[string]string{})
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
os.Remove(tmpFile)
|
||||
})
|
||||
|
||||
It("adds and lists items", func() {
|
||||
_, err := aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
_, err = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "bar"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
res, err := aList.Run(context.TODO(), nil, types.ActionParams{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(res.Metadata["items"]).To(ContainElements("foo", "bar"))
|
||||
})
|
||||
|
||||
It("removes by index", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "bar"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"index": 0})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
res, _ := aList.Run(context.TODO(), nil, types.ActionParams{})
|
||||
Expect(res.Metadata["items"]).To(ConsistOf("bar"))
|
||||
})
|
||||
|
||||
It("removes by value", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "bar"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"value": "bar"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
res, _ := aList.Run(context.TODO(), nil, types.ActionParams{})
|
||||
Expect(res.Metadata["items"]).To(ConsistOf("foo"))
|
||||
})
|
||||
|
||||
It("returns error for out of range index", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"index": 2})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns error for value not found", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"value": "bar"})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns error for empty item", func() {
|
||||
_, err := aAdd.Run(context.TODO(), nil, types.ActionParams{"item": ""})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns error if neither index nor value provided", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user