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:
parent
06d5507f90
commit
71149b054d
1 changed files with 336 additions and 0 deletions
336
pkg/mcp/brain/provider.go
Normal file
336
pkg/mcp/brain/provider.go
Normal 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,
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue