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:
@@ -64,8 +64,9 @@ func (s *Skill) write(ctx context.Context, args json.RawMessage) (json.RawMessag
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ingestArgs struct {
|
type ingestArgs struct {
|
||||||
Content string `json:"content"`
|
Content string `json:"content,omitempty"`
|
||||||
Source string `json:"source"`
|
Source string `json:"source,omitempty"`
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
DryRun bool `json:"dry_run,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 {
|
if err := json.Unmarshal(args, &a); err != nil {
|
||||||
return nil, fmt.Errorf("parse args: %w", err)
|
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 == "" {
|
if s.cfg.IngestSvcURL == "" {
|
||||||
return nil, fmt.Errorf("brain_ingest: INGEST_SVC_URL not configured")
|
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 {
|
type searchArgs struct {
|
||||||
|
|||||||
@@ -63,3 +63,60 @@ func TestHandle_UnknownTool_ReturnsError(t *testing.T) {
|
|||||||
_, err := s.Handle(context.Background(), "brain_unknown", nil)
|
_, err := s.Handle(context.Background(), "brain_unknown", nil)
|
||||||
assert.Error(t, err)
|
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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -58,9 +58,10 @@ func (s *Skill) Tools() []registry.ToolDef {
|
|||||||
tools = append(tools, registry.ToolDef{
|
tools = append(tools, registry.ToolDef{
|
||||||
Name: "brain_ingest",
|
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.",
|
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,
|
"content": str,
|
||||||
"source": map[string]any{"type": "string", "description": "human-readable name for the content, e.g. 'article-on-raft.md'"},
|
"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"},
|
"dry_run": map[string]any{"type": "boolean"},
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user