From 99991c52cf5ce329575f108b9ead3cf0369d6cf9 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Sun, 8 Mar 2026 21:56:45 +0000 Subject: [PATCH] feat: crear shared knowledge tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NewSharedKnowledgeTools genera 4 tools prefijadas shared_knowledge_* - shared_knowledge_search/read/write/list - Descripciones indican que es compartido entre agentes - Tests completos con coexistencia privado/compartido - Issue 0018: Shared Knowledge (fase 2b) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tools/knowledgetools/shared.go | 141 +++++++++++++++++++ tools/knowledgetools/shared_test.go | 202 ++++++++++++++++++++++++++++ 2 files changed, 343 insertions(+) create mode 100644 tools/knowledgetools/shared.go create mode 100644 tools/knowledgetools/shared_test.go diff --git a/tools/knowledgetools/shared.go b/tools/knowledgetools/shared.go new file mode 100644 index 0000000..98c664e --- /dev/null +++ b/tools/knowledgetools/shared.go @@ -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()} + }, + } +} diff --git a/tools/knowledgetools/shared_test.go b/tools/knowledgetools/shared_test.go new file mode 100644 index 0000000..dbec77f --- /dev/null +++ b/tools/knowledgetools/shared_test.go @@ -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") + } +}