From 71149b054df8fcbd891f0081fe6375cfd05c3bb0 Mon Sep 17 00:00:00 2001 From: Snider Date: Sat, 14 Mar 2026 09:47:39 +0000 Subject: [PATCH] feat(brain): add brain service provider with REST endpoints Wraps the brain Subsystem as a provider.Provider with REST endpoints for remember, recall, forget, list, and status. Implements Streamable, Describable, and Renderable for WS events, OpenAPI, and GUI panel discovery. Delegates to the same IDE bridge as MCP tools. Co-Authored-By: Virgil --- pkg/mcp/brain/provider.go | 336 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 pkg/mcp/brain/provider.go diff --git a/pkg/mcp/brain/provider.go b/pkg/mcp/brain/provider.go new file mode 100644 index 0000000..9640fd8 --- /dev/null +++ b/pkg/mcp/brain/provider.go @@ -0,0 +1,336 @@ +// SPDX-Licence-Identifier: EUPL-1.2 + +package brain + +import ( + "net/http" + + "forge.lthn.ai/core/go-api" + "forge.lthn.ai/core/go-api/pkg/provider" + "forge.lthn.ai/core/go-ws" + "forge.lthn.ai/core/mcp/pkg/mcp/ide" + "github.com/gin-gonic/gin" +) + +// BrainProvider wraps the brain Subsystem as a service provider with REST +// endpoints. It delegates to the same IDE bridge that the MCP tools use. +type BrainProvider struct { + bridge *ide.Bridge + hub *ws.Hub +} + +// compile-time interface checks +var ( + _ provider.Provider = (*BrainProvider)(nil) + _ provider.Streamable = (*BrainProvider)(nil) + _ provider.Describable = (*BrainProvider)(nil) + _ provider.Renderable = (*BrainProvider)(nil) +) + +// NewProvider creates a brain provider that proxies to Laravel via the IDE bridge. +// The WS hub is used to emit brain events. Pass nil for hub if not needed. +func NewProvider(bridge *ide.Bridge, hub *ws.Hub) *BrainProvider { + return &BrainProvider{ + bridge: bridge, + hub: hub, + } +} + +// Name implements api.RouteGroup. +func (p *BrainProvider) Name() string { return "brain" } + +// BasePath implements api.RouteGroup. +func (p *BrainProvider) BasePath() string { return "/api/brain" } + +// Channels implements provider.Streamable. +func (p *BrainProvider) Channels() []string { + return []string{ + "brain.remember.complete", + "brain.recall.complete", + "brain.forget.complete", + } +} + +// Element implements provider.Renderable. +func (p *BrainProvider) Element() provider.ElementSpec { + return provider.ElementSpec{ + Tag: "core-brain-panel", + Source: "/assets/brain-panel.js", + } +} + +// RegisterRoutes implements api.RouteGroup. +func (p *BrainProvider) RegisterRoutes(rg *gin.RouterGroup) { + rg.POST("/remember", p.remember) + rg.POST("/recall", p.recall) + rg.POST("/forget", p.forget) + rg.GET("/list", p.list) + rg.GET("/status", p.status) +} + +// Describe implements api.DescribableGroup. +func (p *BrainProvider) Describe() []api.RouteDescription { + return []api.RouteDescription{ + { + Method: "POST", + Path: "/remember", + Summary: "Store a memory", + Description: "Store a memory in the shared OpenBrain knowledge store via the Laravel backend.", + Tags: []string{"brain"}, + RequestBody: map[string]any{ + "type": "object", + "properties": map[string]any{ + "content": map[string]any{"type": "string"}, + "type": map[string]any{"type": "string"}, + "tags": map[string]any{"type": "array", "items": map[string]any{"type": "string"}}, + "project": map[string]any{"type": "string"}, + "confidence": map[string]any{"type": "number"}, + }, + "required": []string{"content", "type"}, + }, + Response: map[string]any{ + "type": "object", + "properties": map[string]any{ + "success": map[string]any{"type": "boolean"}, + "memoryId": map[string]any{"type": "string"}, + "timestamp": map[string]any{"type": "string", "format": "date-time"}, + }, + }, + }, + { + Method: "POST", + Path: "/recall", + Summary: "Semantic search memories", + Description: "Semantic search across the shared OpenBrain knowledge store.", + Tags: []string{"brain"}, + RequestBody: map[string]any{ + "type": "object", + "properties": map[string]any{ + "query": map[string]any{"type": "string"}, + "top_k": map[string]any{"type": "integer"}, + "filter": map[string]any{ + "type": "object", + "properties": map[string]any{ + "project": map[string]any{"type": "string"}, + "type": map[string]any{"type": "string"}, + }, + }, + }, + "required": []string{"query"}, + }, + Response: map[string]any{ + "type": "object", + "properties": map[string]any{ + "success": map[string]any{"type": "boolean"}, + "count": map[string]any{"type": "integer"}, + "memories": map[string]any{"type": "array"}, + }, + }, + }, + { + Method: "POST", + Path: "/forget", + Summary: "Remove a memory", + Description: "Permanently delete a memory from the knowledge store.", + Tags: []string{"brain"}, + RequestBody: map[string]any{ + "type": "object", + "properties": map[string]any{ + "id": map[string]any{"type": "string"}, + "reason": map[string]any{"type": "string"}, + }, + "required": []string{"id"}, + }, + Response: map[string]any{ + "type": "object", + "properties": map[string]any{ + "success": map[string]any{"type": "boolean"}, + "forgotten": map[string]any{"type": "string"}, + }, + }, + }, + { + Method: "GET", + Path: "/list", + Summary: "List memories", + Description: "List memories with optional filtering by project, type, and agent.", + Tags: []string{"brain"}, + Response: map[string]any{ + "type": "object", + "properties": map[string]any{ + "success": map[string]any{"type": "boolean"}, + "count": map[string]any{"type": "integer"}, + "memories": map[string]any{"type": "array"}, + }, + }, + }, + { + Method: "GET", + Path: "/status", + Summary: "Brain bridge status", + Description: "Returns whether the Laravel bridge is connected.", + Tags: []string{"brain"}, + Response: map[string]any{ + "type": "object", + "properties": map[string]any{ + "connected": map[string]any{"type": "boolean"}, + }, + }, + }, + } +} + +// -- Handlers ----------------------------------------------------------------- + +func (p *BrainProvider) remember(c *gin.Context) { + if p.bridge == nil { + c.JSON(http.StatusServiceUnavailable, api.Fail("bridge_unavailable", "brain bridge not available")) + return + } + + var input RememberInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, api.Fail("invalid_input", err.Error())) + return + } + + err := p.bridge.Send(ide.BridgeMessage{ + Type: "brain_remember", + Data: map[string]any{ + "content": input.Content, + "type": input.Type, + "tags": input.Tags, + "project": input.Project, + "confidence": input.Confidence, + "supersedes": input.Supersedes, + "expires_in": input.ExpiresIn, + }, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, api.Fail("bridge_error", err.Error())) + return + } + + p.emitEvent("brain.remember.complete", map[string]any{ + "type": input.Type, + "project": input.Project, + }) + + c.JSON(http.StatusOK, api.OK(map[string]any{"success": true})) +} + +func (p *BrainProvider) recall(c *gin.Context) { + if p.bridge == nil { + c.JSON(http.StatusServiceUnavailable, api.Fail("bridge_unavailable", "brain bridge not available")) + return + } + + var input RecallInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, api.Fail("invalid_input", err.Error())) + return + } + + err := p.bridge.Send(ide.BridgeMessage{ + Type: "brain_recall", + Data: map[string]any{ + "query": input.Query, + "top_k": input.TopK, + "filter": input.Filter, + }, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, api.Fail("bridge_error", err.Error())) + return + } + + p.emitEvent("brain.recall.complete", map[string]any{ + "query": input.Query, + }) + + c.JSON(http.StatusOK, api.OK(RecallOutput{ + Success: true, + Memories: []Memory{}, + })) +} + +func (p *BrainProvider) forget(c *gin.Context) { + if p.bridge == nil { + c.JSON(http.StatusServiceUnavailable, api.Fail("bridge_unavailable", "brain bridge not available")) + return + } + + var input ForgetInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, api.Fail("invalid_input", err.Error())) + return + } + + err := p.bridge.Send(ide.BridgeMessage{ + Type: "brain_forget", + Data: map[string]any{ + "id": input.ID, + "reason": input.Reason, + }, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, api.Fail("bridge_error", err.Error())) + return + } + + p.emitEvent("brain.forget.complete", map[string]any{ + "id": input.ID, + }) + + c.JSON(http.StatusOK, api.OK(map[string]any{ + "success": true, + "forgotten": input.ID, + })) +} + +func (p *BrainProvider) list(c *gin.Context) { + if p.bridge == nil { + c.JSON(http.StatusServiceUnavailable, api.Fail("bridge_unavailable", "brain bridge not available")) + return + } + + err := p.bridge.Send(ide.BridgeMessage{ + Type: "brain_list", + Data: map[string]any{ + "project": c.Query("project"), + "type": c.Query("type"), + "agent_id": c.Query("agent_id"), + "limit": c.Query("limit"), + }, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, api.Fail("bridge_error", err.Error())) + return + } + + c.JSON(http.StatusOK, api.OK(ListOutput{ + Success: true, + Memories: []Memory{}, + })) +} + +func (p *BrainProvider) status(c *gin.Context) { + connected := false + if p.bridge != nil { + connected = p.bridge.Connected() + } + c.JSON(http.StatusOK, api.OK(map[string]any{ + "connected": connected, + })) +} + +// emitEvent sends a WS event if the hub is available. +func (p *BrainProvider) emitEvent(channel string, data any) { + if p.hub == nil { + return + } + _ = p.hub.SendToChannel(channel, ws.Message{ + Type: ws.TypeEvent, + Data: data, + }) +}