- Return error when both path and content are supplied simultaneously - Improve tool description to clearly state the two valid call forms - Add per-field descriptions so LLMs understand what each parameter requires Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
154 lines
4.1 KiB
Go
154 lines
4.1 KiB
Go
// internal/skills/brain/handlers.go
|
|
package brain
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
)
|
|
|
|
// Handle dispatches brain tool calls.
|
|
func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) {
|
|
switch tool {
|
|
case "brain_query":
|
|
return s.query(ctx, args)
|
|
case "brain_write":
|
|
return s.write(ctx, args)
|
|
case "brain_ingest":
|
|
return s.ingest(ctx, args)
|
|
case "brain_search":
|
|
return s.search(ctx, args)
|
|
default:
|
|
return nil, fmt.Errorf("unknown brain tool: %s", tool)
|
|
}
|
|
}
|
|
|
|
type queryArgs struct {
|
|
Query string `json:"query"`
|
|
Limit int `json:"limit,omitempty"`
|
|
}
|
|
|
|
func (s *Skill) query(ctx context.Context, args json.RawMessage) (json.RawMessage, error) {
|
|
var a queryArgs
|
|
if err := json.Unmarshal(args, &a); err != nil {
|
|
return nil, fmt.Errorf("parse args: %w", err)
|
|
}
|
|
if a.Query == "" {
|
|
return nil, fmt.Errorf("query is required")
|
|
}
|
|
if a.Limit == 0 {
|
|
a.Limit = 5
|
|
}
|
|
return s.post(ctx, "/query", a)
|
|
}
|
|
|
|
type writeArgs struct {
|
|
Content string `json:"content"`
|
|
Type string `json:"type,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
Filename string `json:"filename,omitempty"`
|
|
}
|
|
|
|
func (s *Skill) write(ctx context.Context, args json.RawMessage) (json.RawMessage, error) {
|
|
var a writeArgs
|
|
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")
|
|
}
|
|
return s.post(ctx, "/write", a)
|
|
}
|
|
|
|
type ingestArgs struct {
|
|
Content string `json:"content,omitempty"`
|
|
Source string `json:"source,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
DryRun bool `json:"dry_run,omitempty"`
|
|
}
|
|
|
|
func (s *Skill) ingest(ctx context.Context, args json.RawMessage) (json.RawMessage, error) {
|
|
var a ingestArgs
|
|
if err := json.Unmarshal(args, &a); err != nil {
|
|
return nil, fmt.Errorf("parse args: %w", err)
|
|
}
|
|
if s.cfg.IngestSvcURL == "" {
|
|
return nil, fmt.Errorf("brain_ingest: INGEST_SVC_URL not configured")
|
|
}
|
|
if a.Path != "" && a.Content != "" {
|
|
return nil, fmt.Errorf("path and content+source are mutually exclusive: provide one or the other")
|
|
}
|
|
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 {
|
|
Query string `json:"query"`
|
|
Collection string `json:"collection,omitempty"`
|
|
Limit int `json:"limit,omitempty"`
|
|
}
|
|
|
|
func (s *Skill) search(ctx context.Context, args json.RawMessage) (json.RawMessage, error) {
|
|
var a searchArgs
|
|
if err := json.Unmarshal(args, &a); err != nil {
|
|
return nil, fmt.Errorf("parse args: %w", err)
|
|
}
|
|
if a.Query == "" {
|
|
return nil, fmt.Errorf("query is required")
|
|
}
|
|
if a.Limit == 0 {
|
|
a.Limit = 5
|
|
}
|
|
if s.cfg.KBRetrievalURL == "" {
|
|
return nil, fmt.Errorf("brain_search: KB_RETRIEVAL_URL not configured")
|
|
}
|
|
return s.postTo(ctx, s.cfg.KBRetrievalURL+"/api/v1/search", a)
|
|
}
|
|
|
|
func (s *Skill) post(ctx context.Context, path string, body any) (json.RawMessage, error) {
|
|
return s.postTo(ctx, s.cfg.IngestBaseURL+path, body)
|
|
}
|
|
|
|
func (s *Skill) postTo(ctx context.Context, url string, body any) (json.RawMessage, error) {
|
|
b, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal request: %w", err)
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(b))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("build request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("call ingestion server: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
out, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read response: %w", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("ingestion server returned %d: %s", resp.StatusCode, out)
|
|
}
|
|
return json.RawMessage(out), nil
|
|
}
|