From f7cbf5847040d635006c6cf0c955898578bc8a27 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 10:49:22 +0000 Subject: [PATCH] fix(brain): register RFC named actions Co-Authored-By: Virgil --- pkg/brain/actions.go | 334 ++++++++++++++++++++++++++++++ pkg/brain/actions_example_test.go | 20 ++ pkg/brain/actions_test.go | 146 +++++++++++++ pkg/brain/direct.go | 1 + pkg/brain/register.go | 1 + 5 files changed, 502 insertions(+) create mode 100644 pkg/brain/actions.go create mode 100644 pkg/brain/actions_example_test.go create mode 100644 pkg/brain/actions_test.go diff --git a/pkg/brain/actions.go b/pkg/brain/actions.go new file mode 100644 index 0000000..ea8a2bc --- /dev/null +++ b/pkg/brain/actions.go @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package brain + +import ( + "context" + + core "dappco.re/go/core" +) + +type directOptions struct{} + +// subsystem := brain.NewDirect() +// _ = subsystem.OnStartup(context.Background()) +func (s *DirectSubsystem) OnStartup(_ context.Context) core.Result { + if s.ServiceRuntime == nil || s.Core() == nil { + return core.Result{OK: true} + } + + c := s.Core() + c.Action("brain.remember", s.handleRemember).Description = "Store knowledge in OpenBrain" + c.Action("brain.recall", s.handleRecall).Description = "Recall knowledge from OpenBrain" + c.Action("brain.forget", s.handleForget).Description = "Forget knowledge in OpenBrain" + c.Action("brain.list", s.handleList).Description = "List knowledge in OpenBrain" + c.Action("message.send", s.handleSend).Description = "Send a direct message to another agent" + c.Action("message.inbox", s.handleInbox).Description = "Read direct messages for an agent" + c.Action("message.conversation", s.handleConversation).Description = "Read the conversation thread with another agent" + return core.Result{OK: true} +} + +// result := c.Action("brain.remember").Run(ctx, core.NewOptions( +// +// core.Option{Key: "content", Value: "Use OpenBrain for cross-agent context"}, +// core.Option{Key: "type", Value: "architecture"}, +// +// )) +func (s *DirectSubsystem) handleRemember(ctx context.Context, options core.Options) core.Result { + input := RememberInput{ + Content: actionStringValue(options, "content"), + Type: actionStringValue(options, "type"), + Tags: actionStringSliceValue(options, "tags"), + Project: actionStringValue(options, "project"), + Confidence: actionFloatValue(options, "confidence"), + Supersedes: actionStringValue(options, "supersedes"), + ExpiresIn: actionIntValue(options, "expires_in", "expiresIn"), + } + _, output, err := s.remember(ctx, nil, input) + if err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{Value: output, OK: true} +} + +// result := c.Action("brain.recall").Run(ctx, core.NewOptions( +// +// core.Option{Key: "query", Value: "OpenBrain architecture"}, +// core.Option{Key: "top_k", Value: 5}, +// +// )) +func (s *DirectSubsystem) handleRecall(ctx context.Context, options core.Options) core.Result { + input := RecallInput{ + Query: actionStringValue(options, "query"), + TopK: actionIntValue(options, "top_k", "topK"), + Filter: recallFilterFromOptions(options), + } + _, output, err := s.recall(ctx, nil, input) + if err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{Value: output, OK: true} +} + +// result := c.Action("brain.forget").Run(ctx, core.NewOptions( +// +// core.Option{Key: "id", Value: "mem-123"}, +// +// )) +func (s *DirectSubsystem) handleForget(ctx context.Context, options core.Options) core.Result { + input := ForgetInput{ + ID: actionStringValue(options, "id"), + Reason: actionStringValue(options, "reason"), + } + _, output, err := s.forget(ctx, nil, input) + if err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{Value: output, OK: true} +} + +// result := c.Action("brain.list").Run(ctx, core.NewOptions( +// +// core.Option{Key: "project", Value: "agent"}, +// core.Option{Key: "limit", Value: 10}, +// +// )) +func (s *DirectSubsystem) handleList(ctx context.Context, options core.Options) core.Result { + input := ListInput{ + Project: actionStringValue(options, "project"), + Type: actionStringValue(options, "type"), + AgentID: actionStringValue(options, "agent_id", "agent"), + Limit: actionIntValue(options, "limit"), + } + _, output, err := s.list(ctx, nil, input) + if err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{Value: output, OK: true} +} + +// result := c.Action("message.send").Run(ctx, core.NewOptions( +// +// core.Option{Key: "to", Value: "charon"}, +// core.Option{Key: "content", Value: "Deploy complete"}, +// +// )) +func (s *DirectSubsystem) handleSend(ctx context.Context, options core.Options) core.Result { + input := SendInput{ + To: actionStringValue(options, "to"), + Content: actionStringValue(options, "content"), + Subject: actionStringValue(options, "subject"), + } + _, output, err := s.sendMessage(ctx, nil, input) + if err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{Value: output, OK: true} +} + +// result := c.Action("message.inbox").Run(ctx, core.NewOptions( +// +// core.Option{Key: "agent", Value: "cladius"}, +// +// )) +func (s *DirectSubsystem) handleInbox(ctx context.Context, options core.Options) core.Result { + input := InboxInput{ + Agent: actionStringValue(options, "agent"), + } + _, output, err := s.inbox(ctx, nil, input) + if err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{Value: output, OK: true} +} + +// result := c.Action("message.conversation").Run(ctx, core.NewOptions( +// +// core.Option{Key: "agent", Value: "charon"}, +// +// )) +func (s *DirectSubsystem) handleConversation(ctx context.Context, options core.Options) core.Result { + input := ConversationInput{ + Agent: actionStringValue(options, "agent"), + } + _, output, err := s.conversation(ctx, nil, input) + if err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{Value: output, OK: true} +} + +func recallFilterFromOptions(options core.Options) RecallFilter { + filter := recallFilterValue(actionOptionValue(options, "filter")) + if filter.Project == "" { + filter.Project = actionStringValue(options, "project") + } + if filter.Type == nil { + filter.Type = actionOptionValue(options, "type") + } + if filter.AgentID == "" { + filter.AgentID = actionStringValue(options, "agent_id", "agent") + } + if filter.MinConfidence == 0 { + filter.MinConfidence = actionFloatValue(options, "min_confidence", "minConfidence") + } + return filter +} + +func recallFilterValue(value any) RecallFilter { + switch typed := value.(type) { + case RecallFilter: + return typed + case map[string]any: + return RecallFilter{ + Project: actionStringFromAny(typed["project"]), + Type: typed["type"], + AgentID: actionStringFromAny(typed["agent_id"]), + MinConfidence: actionFloatFromAny(typed["min_confidence"]), + } + case map[string]string: + return RecallFilter{ + Project: actionStringFromAny(typed["project"]), + Type: typed["type"], + AgentID: actionStringFromAny(typed["agent_id"]), + } + default: + if text := actionStringFromAny(value); text != "" { + return RecallFilter{Type: text} + } + } + return RecallFilter{} +} + +func actionOptionValue(options core.Options, keys ...string) any { + for _, key := range keys { + result := options.Get(key) + if result.OK { + return result.Value + } + } + return nil +} + +func actionStringValue(options core.Options, keys ...string) string { + return actionStringFromAny(actionOptionValue(options, keys...)) +} + +func actionIntValue(options core.Options, keys ...string) int { + return actionIntFromAny(actionOptionValue(options, keys...)) +} + +func actionFloatValue(options core.Options, keys ...string) float64 { + return actionFloatFromAny(actionOptionValue(options, keys...)) +} + +func actionStringSliceValue(options core.Options, keys ...string) []string { + return actionStringSliceFromAny(actionOptionValue(options, keys...)) +} + +func actionStringFromAny(value any) string { + switch typed := value.(type) { + case string: + return core.Trim(typed) + case int: + return core.Sprint(typed) + case int64: + return core.Sprint(typed) + case float64: + return core.Sprint(int(typed)) + case bool: + return core.Sprint(typed) + } + return "" +} + +func actionIntFromAny(value any) int { + switch typed := value.(type) { + case int: + return typed + case int64: + return int(typed) + case float64: + return int(typed) + case string: + trimmed := core.Trim(typed) + if trimmed == "" { + return 0 + } + var parsed int + if result := core.JSONUnmarshalString(core.Concat("{\"n\":", trimmed, "}"), &struct { + N *int `json:"n"` + }{N: &parsed}); result.OK { + return parsed + } + } + return 0 +} + +func actionFloatFromAny(value any) float64 { + switch typed := value.(type) { + case float64: + return typed + case float32: + return float64(typed) + case int: + return float64(typed) + case int64: + return float64(typed) + case string: + trimmed := core.Trim(typed) + if trimmed == "" { + return 0 + } + var parsed float64 + if result := core.JSONUnmarshalString(core.Concat("{\"n\":", trimmed, "}"), &struct { + N *float64 `json:"n"` + }{N: &parsed}); result.OK { + return parsed + } + } + return 0 +} + +func actionStringSliceFromAny(value any) []string { + switch typed := value.(type) { + case []string: + return cleanActionStrings(typed) + case []any: + var values []string + for _, item := range typed { + if text := actionStringFromAny(item); text != "" { + values = append(values, text) + } + } + return cleanActionStrings(values) + case string: + trimmed := core.Trim(typed) + if trimmed == "" { + return nil + } + if core.HasPrefix(trimmed, "[") { + var values []string + if result := core.JSONUnmarshalString(trimmed, &values); result.OK { + return cleanActionStrings(values) + } + } + return cleanActionStrings(core.Split(trimmed, ",")) + default: + if text := actionStringFromAny(value); text != "" { + return []string{text} + } + } + return nil +} + +func cleanActionStrings(values []string) []string { + var cleaned []string + for _, value := range values { + trimmed := core.Trim(value) + if trimmed != "" { + cleaned = append(cleaned, trimmed) + } + } + return cleaned +} diff --git a/pkg/brain/actions_example_test.go b/pkg/brain/actions_example_test.go new file mode 100644 index 0000000..64dbd63 --- /dev/null +++ b/pkg/brain/actions_example_test.go @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package brain + +import ( + "context" + + core "dappco.re/go/core" +) + +func ExampleRegister_actions() { + c := core.New(core.WithService(Register)) + c.ServiceStartup(context.Background(), nil) + + core.Println(c.Action("brain.list").Exists()) + core.Println(c.Action("message.send").Exists()) + // Output: + // true + // true +} diff --git a/pkg/brain/actions_test.go b/pkg/brain/actions_test.go new file mode 100644 index 0000000..0a8f9fb --- /dev/null +++ b/pkg/brain/actions_test.go @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package brain + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + core "dappco.re/go/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestActions_OnStartup_Good(t *testing.T) { + t.Setenv("CORE_BRAIN_URL", "https://api.lthn.sh") + t.Setenv("CORE_BRAIN_KEY", "test-key") + + c := core.New(core.WithService(Register)) + result := c.ServiceStartup(context.Background(), nil) + require.True(t, result.OK) + + assert.True(t, c.Action("brain.remember").Exists()) + assert.True(t, c.Action("brain.recall").Exists()) + assert.True(t, c.Action("brain.forget").Exists()) + assert.True(t, c.Action("brain.list").Exists()) + assert.True(t, c.Action("message.send").Exists()) + assert.True(t, c.Action("message.inbox").Exists()) + assert.True(t, c.Action("message.conversation").Exists()) +} + +func TestActions_HandleList_Good(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "/v1/brain/list", r.URL.Path) + assert.Equal(t, "agent", r.URL.Query().Get("project")) + assert.Equal(t, "decision", r.URL.Query().Get("type")) + assert.Equal(t, "cladius", r.URL.Query().Get("agent_id")) + assert.Equal(t, "2", r.URL.Query().Get("limit")) + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(core.JSONMarshalString(map[string]any{ + "memories": []any{ + map[string]any{ + "id": "mem-1", + "content": "Use brain.list for filtered history", + "type": "decision", + "project": "agent", + "agent_id": "cladius", + "confidence": 0.9, + "created_at": "2026-03-31T00:00:00Z", + "updated_at": "2026-03-31T00:00:00Z", + }, + }, + }))) + })) + defer srv.Close() + + t.Setenv("CORE_BRAIN_URL", srv.URL) + t.Setenv("CORE_BRAIN_KEY", "test-key") + + c := core.New(core.WithService(Register)) + result := c.ServiceStartup(context.Background(), nil) + require.True(t, result.OK) + + actionResult := c.Action("brain.list").Run(context.Background(), core.NewOptions( + core.Option{Key: "project", Value: "agent"}, + core.Option{Key: "type", Value: "decision"}, + core.Option{Key: "agent_id", Value: "cladius"}, + core.Option{Key: "limit", Value: 2}, + )) + require.True(t, actionResult.OK) + + output, ok := actionResult.Value.(ListOutput) + require.True(t, ok) + assert.True(t, output.Success) + assert.Equal(t, 1, output.Count) + require.Len(t, output.Memories, 1) + assert.Equal(t, "mem-1", output.Memories[0].ID) +} + +func TestActions_HandleList_Bad(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"error":"down"}`)) + })) + defer srv.Close() + + t.Setenv("CORE_BRAIN_URL", srv.URL) + t.Setenv("CORE_BRAIN_KEY", "test-key") + + c := core.New(core.WithService(Register)) + result := c.ServiceStartup(context.Background(), nil) + require.True(t, result.OK) + + actionResult := c.Action("brain.list").Run(context.Background(), core.NewOptions()) + require.False(t, actionResult.OK) + err, ok := actionResult.Value.(error) + require.True(t, ok) + assert.Contains(t, err.Error(), "API call failed") +} + +func TestActions_HandleRecall_Ugly_FilterMap(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/v1/brain/recall", r.URL.Path) + + var body map[string]any + require.True(t, core.JSONUnmarshalString(core.ReadAll(r.Body).Value.(string), &body).OK) + assert.Equal(t, "architecture", body["query"]) + assert.Equal(t, float64(3), body["top_k"]) + assert.Equal(t, "agent", body["project"]) + assert.Equal(t, "decision", body["type"]) + assert.Equal(t, "clotho", body["agent_id"]) + assert.Equal(t, 0.75, body["min_confidence"]) + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(core.JSONMarshalString(map[string]any{"memories": []any{}}))) + })) + defer srv.Close() + + t.Setenv("CORE_BRAIN_URL", srv.URL) + t.Setenv("CORE_BRAIN_KEY", "test-key") + + c := core.New(core.WithService(Register)) + result := c.ServiceStartup(context.Background(), nil) + require.True(t, result.OK) + + actionResult := c.Action("brain.recall").Run(context.Background(), core.NewOptions( + core.Option{Key: "query", Value: "architecture"}, + core.Option{Key: "top_k", Value: 3}, + core.Option{Key: "filter", Value: map[string]any{ + "project": "agent", + "type": "decision", + "agent_id": "clotho", + "min_confidence": 0.75, + }}, + )) + require.True(t, actionResult.OK) + + output, ok := actionResult.Value.(RecallOutput) + require.True(t, ok) + assert.True(t, output.Success) + assert.Equal(t, 0, output.Count) +} diff --git a/pkg/brain/direct.go b/pkg/brain/direct.go index eb37d99..bfb2854 100644 --- a/pkg/brain/direct.go +++ b/pkg/brain/direct.go @@ -16,6 +16,7 @@ import ( // subsystem := brain.NewDirect() // core.Println(subsystem.Name()) // "brain" type DirectSubsystem struct { + *core.ServiceRuntime[directOptions] apiURL string apiKey string } diff --git a/pkg/brain/register.go b/pkg/brain/register.go index 5d8fee8..d37f0a8 100644 --- a/pkg/brain/register.go +++ b/pkg/brain/register.go @@ -11,5 +11,6 @@ import ( // core.Println(subsystem.OK) // true func Register(c *core.Core) core.Result { subsystem := NewDirect() + subsystem.ServiceRuntime = core.NewServiceRuntime(c, directOptions{}) return core.Result{Value: subsystem, OK: true} }