merge: issue/0018-shared-knowledge — sistema de conocimiento compartido
Sistema completo de shared knowledge que permite colaboración entre agentes:
**Implementado:**
- Config SharedKnowledge en schema.go (enabled, dir, db_path)
- WAL mode en FileStore para concurrencia entre procesos
- Tools compartidas: shared_knowledge_{search,read,write,list}
- Integración en agents/runtime.go con instanciación y registro
- Carpeta knowledges/ con README explicativo
- Documentación en prompts de agentes
- Tests completos con coexistencia privado/compartido
- .gitignore actualizado (knowledges/data/)
- Estructura actualizada en CLAUDE.md
**Arquitectura:**
- Reutiliza FileStore existente con directorio compartido
- WAL mode permite múltiples lectores + single writer
- Tools prefijadas shared_knowledge_* vs knowledge_* (privado)
- Los .md se commitean, la DB se reconstruye con Sync()
Issue: dev/issues/completed/0018-shared-knowledge.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,7 @@ security/ grupos de usuarios/agentes + politicas de permisos (YAMLs
|
||||
cmd/launcher/ entrypoint principal (rulesRegistry)
|
||||
cmd/agentctl/ CLI de gestion
|
||||
crons/ catálogo de automatizaciones nombradas (schedule.yaml + prompts)
|
||||
knowledges/ base de conocimiento compartida entre agentes (*.md + SQLite FTS5)
|
||||
dev-scripts/server/ start, stop, restart, ps, logs, dashboard
|
||||
dev-scripts/agent/ new, register, verify, avatar, remove, list
|
||||
dev-scripts/cron/ new, list, apply — gestión de automatizaciones cron
|
||||
|
||||
@@ -9,6 +9,9 @@ run/*.txt
|
||||
|
||||
logs/
|
||||
|
||||
# Shared knowledge DB (markdown files are tracked, DB is rebuilt on sync)
|
||||
knowledges/data/
|
||||
|
||||
# E2E tests
|
||||
e2e/node_modules/
|
||||
e2e/test-results/
|
||||
|
||||
@@ -12,10 +12,22 @@ Eres un asistente conversacional amigable y directo. Operas en Matrix, respondie
|
||||
|
||||
## Herramientas disponibles
|
||||
- `current_time`: Devuelve la fecha y hora actual del servidor. Úsala cuando alguien pregunte por la hora, fecha, o necesites contexto temporal.
|
||||
- `knowledge_search`: Busca documentos en tu base de conocimiento por palabras clave.
|
||||
- `knowledge_read`: Lee el contenido completo de un documento por su slug.
|
||||
- `knowledge_write`: Crea o actualiza un documento de conocimiento.
|
||||
- `knowledge_list`: Lista todos los documentos disponibles.
|
||||
|
||||
### Knowledge privado (tu base personal)
|
||||
- `knowledge_search`: Busca documentos en **tu** base de conocimiento privada.
|
||||
- `knowledge_read`: Lee el contenido completo de un documento en **tu** base privada.
|
||||
- `knowledge_write`: Crea o actualiza un documento en **tu** base privada.
|
||||
- `knowledge_list`: Lista todos los documentos en **tu** base privada.
|
||||
|
||||
### Knowledge compartido (visible para todos los agentes)
|
||||
- `shared_knowledge_search`: Busca en la base compartida entre **todos los agentes**.
|
||||
- `shared_knowledge_read`: Lee un documento compartido que otros agentes pueden haber escrito.
|
||||
- `shared_knowledge_write`: Escribe en la base compartida para que otros agentes lo vean.
|
||||
- `shared_knowledge_list`: Lista documentos compartidos entre agentes.
|
||||
|
||||
**¿Cuándo usar cada una?**
|
||||
- Usa **knowledge privado** para información específica de tu rol o contexto personal.
|
||||
- Usa **shared knowledge** cuando quieras colaborar con otros agentes, compartir información investigada, o consultar lo que otros han registrado.
|
||||
|
||||
## Estilo
|
||||
- Respuestas concisas por defecto. Si necesitas extensión, pregunta primero.
|
||||
|
||||
+60
-20
@@ -92,6 +92,9 @@ type Agent struct {
|
||||
// Knowledge store — non-nil when knowledge is enabled
|
||||
knowledgeStore *shellknowledge.FileStore
|
||||
|
||||
// Shared knowledge store — non-nil when shared_knowledge is enabled
|
||||
sharedKnowledgeStore *shellknowledge.FileStore
|
||||
|
||||
// Sanitization options — nil when sanitization is disabled
|
||||
sanitizeOpts *sanitize.Options
|
||||
|
||||
@@ -235,6 +238,29 @@ func New(cfg *config.AgentConfig, rules []decision.Rule, agentACL acl.ACL, logge
|
||||
}
|
||||
}
|
||||
|
||||
// Shared knowledge store
|
||||
var sharedKStore *shellknowledge.FileStore
|
||||
if cfg.Tools.SharedKnowledge.Enabled {
|
||||
sharedDir := cfg.Tools.SharedKnowledge.Dir
|
||||
if sharedDir == "" {
|
||||
sharedDir = "knowledges"
|
||||
}
|
||||
sharedDBPath := cfg.Tools.SharedKnowledge.DBPath
|
||||
if sharedDBPath == "" {
|
||||
sharedDBPath = "knowledges/data/knowledge.db"
|
||||
}
|
||||
var skErr error
|
||||
sharedKStore, skErr = shellknowledge.New(sharedDir, sharedDBPath, logger)
|
||||
if skErr != nil {
|
||||
logger.Error("shared_knowledge_store_init_failed", "err", skErr)
|
||||
} else {
|
||||
if syncErr := sharedKStore.Sync(context.Background()); syncErr != nil {
|
||||
logger.Error("shared_knowledge_sync_failed", "err", syncErr)
|
||||
}
|
||||
logger.Info("shared knowledge enabled", "dir", sharedDir, "db", sharedDBPath)
|
||||
}
|
||||
}
|
||||
|
||||
if !agentACL.Empty() {
|
||||
logger.Info("acl enabled (centralized security policy)")
|
||||
}
|
||||
@@ -252,7 +278,7 @@ func New(cfg *config.AgentConfig, rules []decision.Rule, agentACL acl.ACL, logge
|
||||
}
|
||||
|
||||
// Tool registry — register tools enabled in config
|
||||
toolReg := buildToolRegistry(cfg, sshExec, matrixClient, memStore, kStore, mcpManager, roomCtx, logger)
|
||||
toolReg := buildToolRegistry(cfg, sshExec, matrixClient, memStore, kStore, sharedKStore, mcpManager, roomCtx, logger)
|
||||
|
||||
// Rate limiting for tools
|
||||
if cfg.Security.ToolRateLimit.Enabled {
|
||||
@@ -278,25 +304,26 @@ func New(cfg *config.AgentConfig, rules []decision.Rule, agentACL acl.ACL, logge
|
||||
}
|
||||
|
||||
a := &Agent{
|
||||
cfg: cfg,
|
||||
acl: agentACL,
|
||||
rules: rules,
|
||||
llm: llmFunc,
|
||||
matrix: matrixClient,
|
||||
runner: runner,
|
||||
toolReg: toolReg,
|
||||
logger: logger,
|
||||
cryptoStore: cryptoStore,
|
||||
mcpManager: mcpManager,
|
||||
done: make(chan struct{}),
|
||||
commands: make(map[string]CommandHandler),
|
||||
cmdAliases: command.BuiltinNames(),
|
||||
startTime: time.Now(),
|
||||
windows: make(map[string]memory.Window),
|
||||
memStore: memStore,
|
||||
knowledgeStore: kStore,
|
||||
windowSize: windowSize,
|
||||
roomCtx: roomCtx,
|
||||
cfg: cfg,
|
||||
acl: agentACL,
|
||||
rules: rules,
|
||||
llm: llmFunc,
|
||||
matrix: matrixClient,
|
||||
runner: runner,
|
||||
toolReg: toolReg,
|
||||
logger: logger,
|
||||
cryptoStore: cryptoStore,
|
||||
mcpManager: mcpManager,
|
||||
done: make(chan struct{}),
|
||||
commands: make(map[string]CommandHandler),
|
||||
cmdAliases: command.BuiltinNames(),
|
||||
startTime: time.Now(),
|
||||
windows: make(map[string]memory.Window),
|
||||
memStore: memStore,
|
||||
knowledgeStore: kStore,
|
||||
sharedKnowledgeStore: sharedKStore,
|
||||
windowSize: windowSize,
|
||||
roomCtx: roomCtx,
|
||||
}
|
||||
|
||||
// Configure sanitization if enabled
|
||||
@@ -417,6 +444,9 @@ func (a *Agent) Run(ctx context.Context) error {
|
||||
if a.knowledgeStore != nil {
|
||||
defer a.knowledgeStore.Close()
|
||||
}
|
||||
if a.sharedKnowledgeStore != nil {
|
||||
defer a.sharedKnowledgeStore.Close()
|
||||
}
|
||||
if a.mcpManager != nil {
|
||||
defer a.mcpManager.Close()
|
||||
}
|
||||
@@ -999,6 +1029,7 @@ func buildToolRegistry(
|
||||
matrixClient *matrix.Client,
|
||||
memStore memory.Store,
|
||||
kStore *shellknowledge.FileStore,
|
||||
sharedKStore *shellknowledge.FileStore,
|
||||
mcpManager *shellmcp.Manager,
|
||||
roomCtx *toolmemory.RoomContext,
|
||||
logger *slog.Logger,
|
||||
@@ -1051,6 +1082,15 @@ func buildToolRegistry(
|
||||
logger.Debug("registered knowledge tools")
|
||||
}
|
||||
|
||||
// Shared knowledge tools
|
||||
if cfg.Tools.SharedKnowledge.Enabled && sharedKStore != nil {
|
||||
sharedTools := toolknowledge.NewSharedKnowledgeTools(sharedKStore)
|
||||
for _, tool := range sharedTools {
|
||||
reg.Register(tool)
|
||||
}
|
||||
logger.Debug("registered shared knowledge tools", "count", len(sharedTools))
|
||||
}
|
||||
|
||||
// MCP tools — register tools from all connected MCP servers
|
||||
if mcpManager != nil {
|
||||
for serverName, mcpClient := range mcpManager.AllClients() {
|
||||
|
||||
@@ -22,7 +22,7 @@ afectados y notas de implementacion.
|
||||
| 15 | Multi-platform Telegram | [0015-multi-platform-telegram.md](0015-multi-platform-telegram.md) | pendiente |
|
||||
| 16 | Skills system | [0016-skills-system.md](0016-skills-system.md) | pendiente |
|
||||
| 17 | MCP client tools | [0017-mcp-client-tools.md](completed/0017-mcp-client-tools.md) | completado |
|
||||
| 18 | Shared knowledge | [0018-shared-knowledge.md](0018-shared-knowledge.md) | pendiente |
|
||||
| 18 | Shared knowledge | [0018-shared-knowledge.md](completed/0018-shared-knowledge.md) | completado |
|
||||
| 19 | Prompt injection hardening | [0019-prompt-injection-hardening.md](completed/0019-prompt-injection-hardening.md) | completado |
|
||||
| 20 | Aislar claude -p del repo | [0020-claude-code-sandbox.md](completed/0020-claude-code-sandbox.md) | completado |
|
||||
| 21 | Threads default config | (completado via branch) | completado |
|
||||
|
||||
@@ -121,14 +121,15 @@ type LLMRateLimitCfg struct {
|
||||
// ── Tools ─────────────────────────────────────────────────────────────────
|
||||
|
||||
type ToolsCfg struct {
|
||||
SSH SSHToolCfg `yaml:"ssh"`
|
||||
HTTP HTTPToolCfg `yaml:"http"`
|
||||
Scripts ScriptsCfg `yaml:"scripts"`
|
||||
FileOps FileOpsCfg `yaml:"file_ops"`
|
||||
Matrix MatrixToolCfg `yaml:"matrix_send"`
|
||||
MCP MCPToolCfg `yaml:"mcp"`
|
||||
Memory MemoryToolCfg `yaml:"memory"`
|
||||
Knowledge KnowledgeToolCfg `yaml:"knowledge"`
|
||||
SSH SSHToolCfg `yaml:"ssh"`
|
||||
HTTP HTTPToolCfg `yaml:"http"`
|
||||
Scripts ScriptsCfg `yaml:"scripts"`
|
||||
FileOps FileOpsCfg `yaml:"file_ops"`
|
||||
Matrix MatrixToolCfg `yaml:"matrix_send"`
|
||||
MCP MCPToolCfg `yaml:"mcp"`
|
||||
Memory MemoryToolCfg `yaml:"memory"`
|
||||
Knowledge KnowledgeToolCfg `yaml:"knowledge"`
|
||||
SharedKnowledge SharedKnowledgeToolCfg `yaml:"shared_knowledge"`
|
||||
}
|
||||
|
||||
type MatrixToolCfg struct {
|
||||
@@ -140,6 +141,12 @@ type KnowledgeToolCfg struct {
|
||||
Dir string `yaml:"dir"` // default: "./knowledge" (relative to agent dir)
|
||||
}
|
||||
|
||||
type SharedKnowledgeToolCfg struct {
|
||||
Enabled bool `yaml:"enabled"` // default false
|
||||
Dir string `yaml:"dir"` // default "knowledges" (relative to project root)
|
||||
DBPath string `yaml:"db_path"` // default "knowledges/data/knowledge.db"
|
||||
}
|
||||
|
||||
type SSHToolCfg struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
AllowedTargets []string `yaml:"allowed_targets"`
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# Shared Knowledge Base
|
||||
|
||||
Esta carpeta contiene la **base de conocimiento compartida** entre todos los agentes del sistema.
|
||||
|
||||
## Propósito
|
||||
|
||||
Los agentes pueden leer, escribir y buscar documentos en esta carpeta usando las tools `shared_knowledge_*`. Esto permite que múltiples agentes colaboren acumulando y consultando conocimiento común.
|
||||
|
||||
## Funcionamiento
|
||||
|
||||
- **Documentos**: Los archivos `.md` en este directorio son los documentos de conocimiento compartidos entre agentes.
|
||||
- **Índice FTS5**: Los documentos se indexan automáticamente en `data/knowledge.db` (SQLite con Full-Text Search).
|
||||
- **Sincronización**: El índice se actualiza al arrancar cada agente con `Sync()`.
|
||||
- **WAL mode**: El DB usa WAL (Write-Ahead Logging) para permitir lecturas y escrituras concurrentes entre múltiples procesos.
|
||||
|
||||
## Tools disponibles
|
||||
|
||||
Los agentes con `tools.shared_knowledge.enabled: true` tienen acceso a:
|
||||
|
||||
- `shared_knowledge_search` — buscar documentos por query
|
||||
- `shared_knowledge_read` — leer un documento por slug
|
||||
- `shared_knowledge_write` — crear o actualizar un documento
|
||||
- `shared_knowledge_list` — listar todos los documentos compartidos
|
||||
|
||||
## Diferencia con knowledge privado
|
||||
|
||||
Cada agente puede tener **dos bases de conocimiento**:
|
||||
|
||||
1. **Knowledge privado** (`agents/<id>/knowledge/`): solo visible para ese agente, tools `knowledge_*`
|
||||
2. **Knowledge compartido** (`knowledges/`): visible para todos los agentes con shared_knowledge habilitado, tools `shared_knowledge_*`
|
||||
|
||||
## Ejemplo de flujo
|
||||
|
||||
```
|
||||
1. agente-A recibe: "investiga X y guarda lo que encuentres"
|
||||
→ LLM usa shared_knowledge_write(slug: "investigacion-x", content: "...")
|
||||
→ Se escribe knowledges/investigacion-x.md + actualiza FTS5
|
||||
|
||||
2. agente-B recibe: "qué sabemos sobre X?"
|
||||
→ LLM usa shared_knowledge_search(query: "X")
|
||||
→ Encuentra el documento que escribió agente-A
|
||||
→ shared_knowledge_read(slug: "investigacion-x")
|
||||
→ Responde con la información
|
||||
```
|
||||
|
||||
## Estructura
|
||||
|
||||
```
|
||||
knowledges/
|
||||
├── README.md ← este archivo
|
||||
├── *.md ← documentos compartidos (commiteados)
|
||||
└── data/
|
||||
├── knowledge.db ← índice SQLite FTS5 (no commiteado)
|
||||
├── knowledge.db-shm
|
||||
└── knowledge.db-wal
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
- Los archivos `.md` se commitean en el repositorio (forman parte del conocimiento compartido del equipo).
|
||||
- El directorio `data/` está en `.gitignore` — el índice se reconstruye automáticamente al arrancar.
|
||||
- No hay control de acceso por agente: cualquier agente con shared_knowledge habilitado puede leer y escribir.
|
||||
- Si dos agentes escriben el mismo slug, el último gana (sobreescritura).
|
||||
@@ -58,6 +58,13 @@ func New(dir, dbPath string, logger *slog.Logger) (*FileStore, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open knowledge db: %w", err)
|
||||
}
|
||||
|
||||
// Enable WAL mode for better concurrency (allows multiple readers + single writer)
|
||||
if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("enable WAL mode: %w", err)
|
||||
}
|
||||
|
||||
if _, err := db.Exec(ftsSchema); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("create knowledge fts5 table: %w", err)
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package knowledgetools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/enmanuel/agents/pkg/knowledge"
|
||||
"github.com/enmanuel/agents/tools"
|
||||
)
|
||||
|
||||
// NewSharedKnowledgeTools creates all shared knowledge tools backed by the given store.
|
||||
// These tools provide access to the shared knowledge base accessible by all agents.
|
||||
func NewSharedKnowledgeTools(store KnowledgeStore) []tools.Tool {
|
||||
return []tools.Tool{
|
||||
newSharedKnowledgeSearch(store),
|
||||
newSharedKnowledgeRead(store),
|
||||
newSharedKnowledgeWrite(store),
|
||||
newSharedKnowledgeList(store),
|
||||
}
|
||||
}
|
||||
|
||||
// newSharedKnowledgeSearch creates a tool that searches the shared knowledge base.
|
||||
func newSharedKnowledgeSearch(store KnowledgeStore) tools.Tool {
|
||||
return tools.Tool{
|
||||
Def: tools.Def{
|
||||
Name: "shared_knowledge_search",
|
||||
Description: "Search the shared knowledge base accessible by all agents. Use this to find information other agents have recorded.",
|
||||
Parameters: []tools.Param{
|
||||
{Name: "query", Type: "string", Description: "Search terms or phrase", Required: true},
|
||||
{Name: "limit", Type: "integer", Description: "Max results (default 5)", Required: false},
|
||||
},
|
||||
},
|
||||
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
||||
query := tools.GetString(args, "query")
|
||||
if query == "" {
|
||||
return tools.Result{Err: fmt.Errorf("shared_knowledge_search: query is required")}
|
||||
}
|
||||
limit := tools.GetInt(args, "limit")
|
||||
if limit <= 0 {
|
||||
limit = 5
|
||||
}
|
||||
|
||||
results, err := store.Search(ctx, query, limit)
|
||||
if err != nil {
|
||||
return tools.Result{Err: fmt.Errorf("shared_knowledge_search: %w", err)}
|
||||
}
|
||||
if len(results) == 0 {
|
||||
return tools.Result{Output: "no documents found in shared knowledge base matching your query"}
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
for i, r := range results {
|
||||
fmt.Fprintf(&sb, "%d. **%s** (`%s`)\n %s\n", i+1, r.Title, r.Slug, r.Snippet)
|
||||
}
|
||||
return tools.Result{Output: sb.String()}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// newSharedKnowledgeRead creates a tool that reads a shared knowledge document.
|
||||
func newSharedKnowledgeRead(store KnowledgeStore) tools.Tool {
|
||||
return tools.Tool{
|
||||
Def: tools.Def{
|
||||
Name: "shared_knowledge_read",
|
||||
Description: "Read the full content of a shared knowledge document by its slug. This document is accessible by all agents.",
|
||||
Parameters: []tools.Param{
|
||||
{Name: "slug", Type: "string", Description: "Document slug (e.g. \"go-patterns\")", Required: true},
|
||||
},
|
||||
},
|
||||
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
||||
slug := tools.GetString(args, "slug")
|
||||
if slug == "" {
|
||||
return tools.Result{Err: fmt.Errorf("shared_knowledge_read: slug is required")}
|
||||
}
|
||||
|
||||
doc, err := store.Get(ctx, slug)
|
||||
if err != nil {
|
||||
return tools.Result{Err: fmt.Errorf("shared_knowledge_read: %w", err)}
|
||||
}
|
||||
return tools.Result{Output: doc.Content}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// newSharedKnowledgeWrite creates a tool that writes a shared knowledge document.
|
||||
func newSharedKnowledgeWrite(store KnowledgeStore) tools.Tool {
|
||||
return tools.Tool{
|
||||
Def: tools.Def{
|
||||
Name: "shared_knowledge_write",
|
||||
Description: "Create or update a shared knowledge document accessible by all agents. Use this to share knowledge with other agents.",
|
||||
Parameters: []tools.Param{
|
||||
{Name: "slug", Type: "string", Description: "Document slug (lowercase, hyphens, e.g. \"matrix-tips\")", Required: true},
|
||||
{Name: "content", Type: "string", Description: "Full markdown content of the document", Required: true},
|
||||
},
|
||||
},
|
||||
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
||||
slug := tools.GetString(args, "slug")
|
||||
content := tools.GetString(args, "content")
|
||||
if slug == "" || content == "" {
|
||||
return tools.Result{Err: fmt.Errorf("shared_knowledge_write: slug and content are required")}
|
||||
}
|
||||
|
||||
err := store.Put(ctx, knowledge.Document{
|
||||
Slug: slug,
|
||||
Content: content,
|
||||
})
|
||||
if err != nil {
|
||||
return tools.Result{Err: fmt.Errorf("shared_knowledge_write: %w", err)}
|
||||
}
|
||||
return tools.Result{Output: fmt.Sprintf("shared document saved: %s (%d bytes)", slug, len(content))}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// newSharedKnowledgeList creates a tool that lists all shared knowledge documents.
|
||||
func newSharedKnowledgeList(store KnowledgeStore) tools.Tool {
|
||||
return tools.Tool{
|
||||
Def: tools.Def{
|
||||
Name: "shared_knowledge_list",
|
||||
Description: "List all documents in the shared knowledge base accessible by all agents.",
|
||||
Parameters: []tools.Param{},
|
||||
},
|
||||
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
||||
docs, err := store.List(ctx)
|
||||
if err != nil {
|
||||
return tools.Result{Err: fmt.Errorf("shared_knowledge_list: %w", err)}
|
||||
}
|
||||
if len(docs) == 0 {
|
||||
return tools.Result{Output: "shared knowledge base is empty"}
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
for _, d := range docs {
|
||||
fmt.Fprintf(&sb, "- `%s`: %s (updated %s)\n",
|
||||
d.Slug, d.Title, d.UpdatedAt.Format("2006-01-02"))
|
||||
}
|
||||
return tools.Result{Output: sb.String()}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package knowledgetools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/enmanuel/agents/pkg/knowledge"
|
||||
)
|
||||
|
||||
func TestNewSharedKnowledgeTools(t *testing.T) {
|
||||
store := newMockKnowledgeStore()
|
||||
tools := NewSharedKnowledgeTools(store)
|
||||
|
||||
if len(tools) != 4 {
|
||||
t.Errorf("expected 4 tools, got %d", len(tools))
|
||||
}
|
||||
|
||||
names := make(map[string]bool)
|
||||
for _, tool := range tools {
|
||||
names[tool.Def.Name] = true
|
||||
}
|
||||
|
||||
expected := []string{
|
||||
"shared_knowledge_search",
|
||||
"shared_knowledge_read",
|
||||
"shared_knowledge_write",
|
||||
"shared_knowledge_list",
|
||||
}
|
||||
|
||||
for _, name := range expected {
|
||||
if !names[name] {
|
||||
t.Errorf("expected tool %q not found", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedKnowledgeSearchTool(t *testing.T) {
|
||||
store := newMockKnowledgeStore()
|
||||
store.docs["shared-doc"] = knowledge.Document{
|
||||
Slug: "shared-doc", Title: "Shared Doc", Content: "This is shared knowledge",
|
||||
}
|
||||
|
||||
tools := NewSharedKnowledgeTools(store)
|
||||
tool := tools[0] // shared_knowledge_search is first
|
||||
|
||||
// Missing query
|
||||
r := tool.Exec(context.Background(), map[string]any{})
|
||||
if r.Err == nil {
|
||||
t.Error("expected error for missing query")
|
||||
}
|
||||
|
||||
// Valid search
|
||||
r = tool.Exec(context.Background(), map[string]any{"query": "shared"})
|
||||
if r.Err != nil {
|
||||
t.Errorf("unexpected error: %v", r.Err)
|
||||
}
|
||||
if r.Output == "" {
|
||||
t.Error("expected non-empty output")
|
||||
}
|
||||
|
||||
// Empty results
|
||||
store2 := newMockKnowledgeStore()
|
||||
tools2 := NewSharedKnowledgeTools(store2)
|
||||
r = tools2[0].Exec(context.Background(), map[string]any{"query": "nothing"})
|
||||
if r.Err != nil {
|
||||
t.Errorf("unexpected error: %v", r.Err)
|
||||
}
|
||||
if r.Output != "no documents found in shared knowledge base matching your query" {
|
||||
t.Errorf("expected empty message, got %q", r.Output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedKnowledgeReadTool(t *testing.T) {
|
||||
store := newMockKnowledgeStore()
|
||||
store.docs["shared-doc"] = knowledge.Document{
|
||||
Slug: "shared-doc", Title: "Shared", Content: "Shared content",
|
||||
}
|
||||
|
||||
tools := NewSharedKnowledgeTools(store)
|
||||
tool := tools[1] // shared_knowledge_read is second
|
||||
|
||||
// Missing slug
|
||||
r := tool.Exec(context.Background(), map[string]any{})
|
||||
if r.Err == nil {
|
||||
t.Error("expected error for missing slug")
|
||||
}
|
||||
|
||||
// Valid read
|
||||
r = tool.Exec(context.Background(), map[string]any{"slug": "shared-doc"})
|
||||
if r.Err != nil {
|
||||
t.Errorf("unexpected error: %v", r.Err)
|
||||
}
|
||||
if r.Output != "Shared content" {
|
||||
t.Errorf("output = %q, want %q", r.Output, "Shared content")
|
||||
}
|
||||
|
||||
// Not found
|
||||
r = tool.Exec(context.Background(), map[string]any{"slug": "nope"})
|
||||
if r.Err == nil {
|
||||
t.Error("expected error for nonexistent doc")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedKnowledgeWriteTool(t *testing.T) {
|
||||
store := newMockKnowledgeStore()
|
||||
tools := NewSharedKnowledgeTools(store)
|
||||
tool := tools[2] // shared_knowledge_write is third
|
||||
|
||||
// Missing params
|
||||
r := tool.Exec(context.Background(), map[string]any{"slug": "test"})
|
||||
if r.Err == nil {
|
||||
t.Error("expected error for missing content")
|
||||
}
|
||||
|
||||
// Valid write
|
||||
r = tool.Exec(context.Background(), map[string]any{
|
||||
"slug": "shared-doc",
|
||||
"content": "# Shared Doc\nShared by agent A",
|
||||
})
|
||||
if r.Err != nil {
|
||||
t.Errorf("unexpected error: %v", r.Err)
|
||||
}
|
||||
if _, ok := store.docs["shared-doc"]; !ok {
|
||||
t.Error("document was not stored")
|
||||
}
|
||||
|
||||
// Verify the output message mentions "shared"
|
||||
if r.Output != "shared document saved: shared-doc (30 bytes)" {
|
||||
t.Errorf("output = %q, want mention of shared", r.Output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedKnowledgeListTool(t *testing.T) {
|
||||
store := newMockKnowledgeStore()
|
||||
tools := NewSharedKnowledgeTools(store)
|
||||
tool := tools[3] // shared_knowledge_list is fourth
|
||||
|
||||
// Empty
|
||||
r := tool.Exec(context.Background(), map[string]any{})
|
||||
if r.Err != nil {
|
||||
t.Errorf("unexpected error: %v", r.Err)
|
||||
}
|
||||
if r.Output != "shared knowledge base is empty" {
|
||||
t.Errorf("expected empty message, got %q", r.Output)
|
||||
}
|
||||
|
||||
// With docs
|
||||
store.docs["shared-doc1"] = knowledge.Document{Slug: "shared-doc1", Title: "Shared 1"}
|
||||
r = tool.Exec(context.Background(), map[string]any{})
|
||||
if r.Err != nil {
|
||||
t.Errorf("unexpected error: %v", r.Err)
|
||||
}
|
||||
if r.Output == "shared knowledge base is empty" {
|
||||
t.Error("expected non-empty output after adding docs")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSharedAndPrivateCoexist verifies that shared and private tools can coexist
|
||||
// with different stores and don't interfere with each other.
|
||||
func TestSharedAndPrivateCoexist(t *testing.T) {
|
||||
privateStore := newMockKnowledgeStore()
|
||||
sharedStore := newMockKnowledgeStore()
|
||||
|
||||
// Write to private store
|
||||
privateStore.docs["private-doc"] = knowledge.Document{
|
||||
Slug: "private-doc", Title: "Private", Content: "Private content",
|
||||
}
|
||||
|
||||
// Write to shared store
|
||||
sharedStore.docs["shared-doc"] = knowledge.Document{
|
||||
Slug: "shared-doc", Title: "Shared", Content: "Shared content",
|
||||
}
|
||||
|
||||
// Verify private has only private doc
|
||||
privateDocs, _ := privateStore.List(context.Background())
|
||||
if len(privateDocs) != 1 || privateDocs[0].Slug != "private-doc" {
|
||||
t.Error("private store should only have private doc")
|
||||
}
|
||||
|
||||
// Verify shared has only shared doc
|
||||
sharedDocs, _ := sharedStore.List(context.Background())
|
||||
if len(sharedDocs) != 1 || sharedDocs[0].Slug != "shared-doc" {
|
||||
t.Error("shared store should only have shared doc")
|
||||
}
|
||||
|
||||
// Verify tools from different stores don't mix data
|
||||
privateTool := NewKnowledgeRead(privateStore)
|
||||
sharedTools := NewSharedKnowledgeTools(sharedStore)
|
||||
sharedTool := sharedTools[1] // shared_knowledge_read
|
||||
|
||||
// Private tool can't read shared doc
|
||||
r := privateTool.Exec(context.Background(), map[string]any{"slug": "shared-doc"})
|
||||
if r.Err == nil {
|
||||
t.Error("private tool should not be able to read shared doc")
|
||||
}
|
||||
|
||||
// Shared tool can't read private doc
|
||||
r = sharedTool.Exec(context.Background(), map[string]any{"slug": "private-doc"})
|
||||
if r.Err == nil {
|
||||
t.Error("shared tool should not be able to read private doc")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user