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 <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-14 09:47:39 +00:00
parent 06d5507f90
commit 71149b054d

336
pkg/mcp/brain/provider.go Normal file
View file

@ -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,
})
}