feat: import agents_and_robots platform as unibots (Matrix-out, unibus transport)

Reemplaza el scaffold del echobot por la plataforma completa de bots traida
desde ~/DataProyects/Github/agents_and_robots tras la operacion Matrix-out:
los bots ya no hablan por Matrix sino por el bus unibus (modelo todo-rooms +
E2E via shell/transportunibus sobre github.com/enmanuel/unibus/pkg/client).

- go.mod: replace de unibus -> ../unibus y de fn-registry -> ../../../.. (paths
  relativos reajustados a la nueva ubicacion dentro de fn_registry).
- app.md: bump a 0.2.0, descripcion + arquitectura + comandos + gotchas reales.
- modulo Go conservado como github.com/enmanuel/agents (sin reescribir imports).

agents_and_robots queda archivado como museo de la era Matrix.
This commit is contained in:
agent
2026-06-07 11:50:13 +02:00
parent bb5b0e09b1
commit fc644ecd6e
308 changed files with 38829 additions and 474 deletions
+138
View File
@@ -0,0 +1,138 @@
package knowledgetools
import (
"context"
"fmt"
"strings"
"github.com/enmanuel/agents/pkg/knowledge"
"github.com/enmanuel/agents/tools"
)
// KnowledgeStore is the subset of knowledge.Store needed by knowledge tools.
type KnowledgeStore interface {
Search(ctx context.Context, query string, limit int) ([]knowledge.SearchResult, error)
Get(ctx context.Context, slug string) (*knowledge.Document, error)
Put(ctx context.Context, doc knowledge.Document) error
List(ctx context.Context) ([]knowledge.Document, error)
}
// NewKnowledgeSearch creates a tool that searches the knowledge base.
func NewKnowledgeSearch(store KnowledgeStore) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: "knowledge_search",
Description: "Search your knowledge base for relevant documents. Returns matching snippets ranked by relevance.",
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("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("knowledge_search: %w", err)}
}
if len(results) == 0 {
return tools.Result{Output: "no documents found 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()}
},
}
}
// NewKnowledgeRead creates a tool that reads a knowledge document.
func NewKnowledgeRead(store KnowledgeStore) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: "knowledge_read",
Description: "Read the full content of a knowledge document by its slug.",
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("knowledge_read: slug is required")}
}
doc, err := store.Get(ctx, slug)
if err != nil {
return tools.Result{Err: fmt.Errorf("knowledge_read: %w", err)}
}
return tools.Result{Output: doc.Content}
},
}
}
// NewKnowledgeWrite creates a tool that writes a knowledge document.
func NewKnowledgeWrite(store KnowledgeStore) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: "knowledge_write",
Description: "Create or update a knowledge document. Use this to save new knowledge or improve existing documents.",
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("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("knowledge_write: %w", err)}
}
return tools.Result{Output: fmt.Sprintf("document saved: %s (%d bytes)", slug, len(content))}
},
}
}
// NewKnowledgeList creates a tool that lists all knowledge documents.
func NewKnowledgeList(store KnowledgeStore) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: "knowledge_list",
Description: "List all documents in your knowledge base with their titles.",
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("knowledge_list: %w", err)}
}
if len(docs) == 0 {
return tools.Result{Output: "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()}
},
}
}
+182
View File
@@ -0,0 +1,182 @@
package knowledgetools
import (
"context"
"testing"
"github.com/enmanuel/agents/pkg/knowledge"
"github.com/enmanuel/agents/tools"
)
// mockKnowledgeStore implements KnowledgeStore for testing.
type mockKnowledgeStore struct {
docs map[string]knowledge.Document
}
func newMockKnowledgeStore() *mockKnowledgeStore {
return &mockKnowledgeStore{docs: make(map[string]knowledge.Document)}
}
func (m *mockKnowledgeStore) Search(_ context.Context, query string, limit int) ([]knowledge.SearchResult, error) {
var results []knowledge.SearchResult
for _, d := range m.docs {
if len(results) >= limit {
break
}
results = append(results, knowledge.SearchResult{
Slug: d.Slug,
Title: d.Title,
Snippet: d.Content[:min(len(d.Content), 50)],
Rank: 1.0,
})
}
return results, nil
}
func (m *mockKnowledgeStore) Get(_ context.Context, slug string) (*knowledge.Document, error) {
d, ok := m.docs[slug]
if !ok {
return nil, &notFoundError{slug}
}
return &d, nil
}
func (m *mockKnowledgeStore) Put(_ context.Context, doc knowledge.Document) error {
m.docs[doc.Slug] = doc
return nil
}
func (m *mockKnowledgeStore) List(_ context.Context) ([]knowledge.Document, error) {
var docs []knowledge.Document
for _, d := range m.docs {
docs = append(docs, d)
}
return docs, nil
}
type notFoundError struct{ slug string }
func (e *notFoundError) Error() string { return "not found: " + e.slug }
func TestKnowledgeSearchTool(t *testing.T) {
store := newMockKnowledgeStore()
store.docs["go-patterns"] = knowledge.Document{
Slug: "go-patterns", Title: "Go Patterns", Content: "Use interfaces",
}
tool := NewKnowledgeSearch(store)
if tool.Def.Name != "knowledge_search" {
t.Errorf("name = %q, want knowledge_search", tool.Def.Name)
}
// 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": "go"})
if r.Err != nil {
t.Errorf("unexpected error: %v", r.Err)
}
if r.Output == "" {
t.Error("expected non-empty output")
}
}
func TestKnowledgeReadTool(t *testing.T) {
store := newMockKnowledgeStore()
store.docs["test-doc"] = knowledge.Document{
Slug: "test-doc", Title: "Test", Content: "Hello world",
}
tool := NewKnowledgeRead(store)
// 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": "test-doc"})
if r.Err != nil {
t.Errorf("unexpected error: %v", r.Err)
}
if r.Output != "Hello world" {
t.Errorf("output = %q, want %q", r.Output, "Hello world")
}
// Not found
r = tool.Exec(context.Background(), map[string]any{"slug": "nope"})
if r.Err == nil {
t.Error("expected error for nonexistent doc")
}
}
func TestKnowledgeWriteTool(t *testing.T) {
store := newMockKnowledgeStore()
tool := NewKnowledgeWrite(store)
// 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": "new-doc",
"content": "# New Doc\nSome content",
})
if r.Err != nil {
t.Errorf("unexpected error: %v", r.Err)
}
if _, ok := store.docs["new-doc"]; !ok {
t.Error("document was not stored")
}
}
func TestKnowledgeListTool(t *testing.T) {
store := newMockKnowledgeStore()
tool := NewKnowledgeList(store)
// Empty
r := tool.Exec(context.Background(), map[string]any{})
if r.Err != nil {
t.Errorf("unexpected error: %v", r.Err)
}
if r.Output != "knowledge base is empty" {
t.Errorf("expected empty message, got %q", r.Output)
}
// With docs
store.docs["doc1"] = knowledge.Document{Slug: "doc1", Title: "Doc 1"}
r = tool.Exec(context.Background(), map[string]any{})
if r.Err != nil {
t.Errorf("unexpected error: %v", r.Err)
}
if r.Output == "knowledge base is empty" {
t.Error("expected non-empty output after adding docs")
}
}
func TestGetInt(t *testing.T) {
tests := []struct {
args map[string]any
key string
want int
}{
{map[string]any{"n": float64(5)}, "n", 5},
{map[string]any{"n": 3}, "n", 3},
{map[string]any{"n": "str"}, "n", 0},
{map[string]any{}, "n", 0},
}
for _, tt := range tests {
got := tools.GetInt(tt.args, tt.key)
if got != tt.want {
t.Errorf("GetInt(%v, %q) = %d, want %d", tt.args, tt.key, got, tt.want)
}
}
}
+141
View File
@@ -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()}
},
}
}
+202
View File
@@ -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")
}
}