feat(brain): add path field to brain_ingest for /ingest-path routing

Adds an optional path field to brain_ingest so Claude can ingest files
or directories directly by path without embedding content in the call.
Routing: path set → /ingest-path; content+source set → /ingest; neither → error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mathias Bergqvist
2026-04-22 23:01:05 +02:00
parent 2f4b577131
commit a6dce972d6
3 changed files with 77 additions and 10 deletions

View File

@@ -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 {

View File

@@ -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")
}

View File

@@ -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"},
}),
})