diff --git a/internal/skills/brain/handlers.go b/internal/skills/brain/handlers.go index c6a825d..ae53e42 100644 --- a/internal/skills/brain/handlers.go +++ b/internal/skills/brain/handlers.go @@ -64,8 +64,9 @@ func (s *Skill) write(ctx context.Context, args json.RawMessage) (json.RawMessag } type ingestArgs struct { - Content string `json:"content"` - Source string `json:"source"` + Content string `json:"content,omitempty"` + Source string `json:"source,omitempty"` + Path string `json:"path,omitempty"` DryRun bool `json:"dry_run,omitempty"` } @@ -74,16 +75,24 @@ func (s *Skill) ingest(ctx context.Context, args json.RawMessage) (json.RawMessa if err := json.Unmarshal(args, &a); err != nil { return nil, fmt.Errorf("parse args: %w", err) } - if a.Content == "" { - return nil, fmt.Errorf("content is required") - } - if a.Source == "" { - return nil, fmt.Errorf("source is required") - } if s.cfg.IngestSvcURL == "" { return nil, fmt.Errorf("brain_ingest: INGEST_SVC_URL not configured") } - return s.postTo(ctx, s.cfg.IngestSvcURL+"/ingest", a) + if a.Path != "" { + return s.postTo(ctx, s.cfg.IngestSvcURL+"/ingest-path", map[string]any{ + "path": a.Path, + "source": a.Source, + "dry_run": a.DryRun, + }) + } + if a.Content != "" && a.Source != "" { + return s.postTo(ctx, s.cfg.IngestSvcURL+"/ingest", map[string]any{ + "content": a.Content, + "source": a.Source, + "dry_run": a.DryRun, + }) + } + return nil, fmt.Errorf("either content+source or path is required") } type searchArgs struct { diff --git a/internal/skills/brain/handlers_test.go b/internal/skills/brain/handlers_test.go index 20b59f3..fa3d2f8 100644 --- a/internal/skills/brain/handlers_test.go +++ b/internal/skills/brain/handlers_test.go @@ -63,3 +63,60 @@ func TestHandle_UnknownTool_ReturnsError(t *testing.T) { _, err := s.Handle(context.Background(), "brain_unknown", nil) assert.Error(t, err) } + +func TestIngest_RoutesToIngestPath(t *testing.T) { + var capturedPath string + var capturedBody map[string]any + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedPath = r.URL.Path + require.NoError(t, json.NewDecoder(r.Body).Decode(&capturedBody)) + _ = json.NewEncoder(w).Encode(map[string]any{"pages": []string{"wiki/foo.md"}}) + })) + defer srv.Close() + + s := brain.New(brain.Config{IngestSvcURL: srv.URL}) + args, _ := json.Marshal(map[string]any{"path": "/tmp/some-file.md"}) + out, err := s.Handle(context.Background(), "brain_ingest", args) + require.NoError(t, err) + + assert.Equal(t, "/ingest-path", capturedPath) + assert.Equal(t, "/tmp/some-file.md", capturedBody["path"]) + + var result map[string]any + require.NoError(t, json.Unmarshal(out, &result)) + pages := result["pages"].([]any) + assert.Len(t, pages, 1) +} + +func TestIngest_RoutesToIngest(t *testing.T) { + var capturedPath string + var capturedBody map[string]any + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedPath = r.URL.Path + require.NoError(t, json.NewDecoder(r.Body).Decode(&capturedBody)) + _ = json.NewEncoder(w).Encode(map[string]any{"pages": []string{"wiki/bar.md"}}) + })) + defer srv.Close() + + s := brain.New(brain.Config{IngestSvcURL: srv.URL}) + args, _ := json.Marshal(map[string]any{"content": "some content", "source": "my-source.md"}) + out, err := s.Handle(context.Background(), "brain_ingest", args) + require.NoError(t, err) + + assert.Equal(t, "/ingest", capturedPath) + assert.Equal(t, "some content", capturedBody["content"]) + assert.Equal(t, "my-source.md", capturedBody["source"]) + + var result map[string]any + require.NoError(t, json.Unmarshal(out, &result)) + pages := result["pages"].([]any) + assert.Len(t, pages, 1) +} + +func TestIngest_MissingRequiredFields(t *testing.T) { + s := brain.New(brain.Config{IngestSvcURL: "http://localhost:3300"}) + args, _ := json.Marshal(map[string]any{}) + _, err := s.Handle(context.Background(), "brain_ingest", args) + require.Error(t, err) + assert.Contains(t, err.Error(), "either content+source or path is required") +} diff --git a/internal/skills/brain/skill.go b/internal/skills/brain/skill.go index 05a9a60..6b6da1f 100644 --- a/internal/skills/brain/skill.go +++ b/internal/skills/brain/skill.go @@ -58,9 +58,10 @@ func (s *Skill) Tools() []registry.ToolDef { tools = append(tools, registry.ToolDef{ Name: "brain_ingest", Description: "Ingest text content into the brain wiki (brain/wiki/). Calls an LLM to produce structured wiki pages. Use for substantial documents, articles, or knowledge worth structuring. Returns the list of wiki pages written.", - InputSchema: schema([]string{"content", "source"}, map[string]any{ + InputSchema: schema([]string{}, map[string]any{ "content": str, "source": map[string]any{"type": "string", "description": "human-readable name for the content, e.g. 'article-on-raft.md'"}, + "path": map[string]any{"type": "string", "description": "absolute path to a file or directory to ingest; if set, content and source are not needed"}, "dry_run": map[string]any{"type": "boolean"}, }), })