agent/pkg/brain/provider.go
Snider 67658ec90c fix: imports forge.lthn.ai/core/mcp → dappco.re/go/mcp
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-24 21:40:04 +00:00

352 lines
9.1 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package brain
import (
"net/http"
"strconv"
"dappco.re/go/core/api"
"dappco.re/go/core/api/pkg/provider"
"dappco.re/go/core/ws"
"dappco.re/go/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.
//
// Usage example:
//
// provider := brain.NewProvider(bridge, hub)
// provider.RegisterRoutes(router.Group("/api/brain"))
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
}
limit := 0
if rawLimit := c.Query("limit"); rawLimit != "" {
parsedLimit, err := strconv.Atoi(rawLimit)
if err != nil {
c.JSON(http.StatusBadRequest, api.Fail("invalid_limit", "limit must be an integer"))
return
}
limit = parsedLimit
}
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": 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,
})
}