- go.mod module name
- All 714 import references across 289 Go files
- VERSION: 0.1.0
- CI DB names: switchboard_core_{ci,dev,test}
- Docker image: gobha/switchboard-core
- Test fixtures: JWT issuer, repo names
- .env.example, docker-compose container name
- Compiles clean (go build exit 0)
152 lines
4.1 KiB
Go
152 lines
4.1 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"switchboard-core/knowledge"
|
|
"switchboard-core/models"
|
|
"switchboard-core/store"
|
|
)
|
|
|
|
// ── workspace_search ────────────────────────
|
|
|
|
type workspaceSearchTool struct {
|
|
workspaceToolBase
|
|
stores store.Stores
|
|
embedder *knowledge.Embedder
|
|
}
|
|
|
|
func (t *workspaceSearchTool) Definition() ToolDef {
|
|
return ToolDef{
|
|
Name: "workspace_search",
|
|
DisplayName: "Workspace Search",
|
|
Description: "Semantic search across workspace files. Finds code, documentation, and text content relevant to a natural language query. Returns matching file paths, content snippets, relevance scores, and approximate line numbers.",
|
|
Category: "workspace",
|
|
Parameters: JSONSchema(map[string]interface{}{
|
|
"query": Prop("string", "Natural language search query (e.g. 'database connection pooling', 'error handling middleware')"),
|
|
"top_k": map[string]interface{}{
|
|
"type": "integer",
|
|
"description": "Maximum number of results to return (default 10, max 20)",
|
|
"default": 10,
|
|
},
|
|
"file_pattern": Prop("string", "Optional glob pattern to filter results by file path (e.g. '*.go', 'src/*.ts'). Applied post-search."),
|
|
}, []string{"query"}),
|
|
}
|
|
}
|
|
|
|
func (t *workspaceSearchTool) Execute(ctx context.Context, execCtx ExecutionContext, argsJSON string) (string, error) {
|
|
var args struct {
|
|
Query string `json:"query"`
|
|
TopK int `json:"top_k"`
|
|
FilePattern string `json:"file_pattern"`
|
|
}
|
|
if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
|
|
return "", fmt.Errorf("invalid arguments: %w", err)
|
|
}
|
|
|
|
if args.Query == "" {
|
|
return "", fmt.Errorf("query is required")
|
|
}
|
|
if args.TopK <= 0 {
|
|
args.TopK = 10
|
|
}
|
|
if args.TopK > 20 {
|
|
args.TopK = 20
|
|
}
|
|
|
|
// Resolve workspace
|
|
w, err := loadWorkspace(ctx, t.stores, execCtx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if !w.IndexingEnabled {
|
|
return "", fmt.Errorf("workspace indexing is disabled for this workspace")
|
|
}
|
|
|
|
// Embed the query
|
|
embedResult, err := t.embedder.EmbedChunks(ctx, execCtx.UserID, nil, []string{args.Query})
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to embed query: %w", err)
|
|
}
|
|
if len(embedResult.Vectors) == 0 {
|
|
return "", fmt.Errorf("embedding returned no vectors")
|
|
}
|
|
|
|
queryVec := embedResult.Vectors[0]
|
|
|
|
// Fetch more if we'll post-filter by glob
|
|
searchLimit := args.TopK
|
|
if args.FilePattern != "" {
|
|
searchLimit = args.TopK * 3
|
|
if searchLimit > 50 {
|
|
searchLimit = 50
|
|
}
|
|
}
|
|
|
|
results, err := t.stores.Workspaces.SimilaritySearch(ctx, w.ID, queryVec, 0.3, searchLimit)
|
|
if err != nil {
|
|
return "", fmt.Errorf("search failed: %w", err)
|
|
}
|
|
|
|
// Apply glob filter if specified
|
|
if args.FilePattern != "" {
|
|
results = filterByGlob(results, args.FilePattern)
|
|
}
|
|
|
|
// Cap at top_k
|
|
if len(results) > args.TopK {
|
|
results = results[:args.TopK]
|
|
}
|
|
|
|
// Format response
|
|
formatted := make([]map[string]interface{}, len(results))
|
|
for i, r := range results {
|
|
m := map[string]interface{}{
|
|
"file_path": r.FilePath,
|
|
"content": r.Content,
|
|
"score": fmt.Sprintf("%.3f", r.Score),
|
|
}
|
|
if r.LineHint > 0 {
|
|
m["line_hint"] = r.LineHint
|
|
}
|
|
formatted[i] = m
|
|
}
|
|
|
|
resp := map[string]interface{}{
|
|
"query": args.Query,
|
|
"count": len(formatted),
|
|
"results": formatted,
|
|
}
|
|
if args.FilePattern != "" {
|
|
resp["file_pattern"] = args.FilePattern
|
|
}
|
|
if len(formatted) == 0 {
|
|
resp["message"] = "No matching results found. Try a different query or check that workspace files have been indexed."
|
|
}
|
|
|
|
b, _ := json.Marshal(resp)
|
|
return string(b), nil
|
|
}
|
|
|
|
// filterByGlob filters search results by a glob pattern.
|
|
func filterByGlob(results []models.WorkspaceChunkResult, pattern string) []models.WorkspaceChunkResult {
|
|
var filtered []models.WorkspaceChunkResult
|
|
for _, r := range results {
|
|
// Try matching against basename
|
|
matched, _ := filepath.Match(pattern, filepath.Base(r.FilePath))
|
|
if !matched && strings.Contains(pattern, "/") {
|
|
// Try full path match if pattern contains directory separator
|
|
matched, _ = filepath.Match(pattern, r.FilePath)
|
|
}
|
|
if matched {
|
|
filtered = append(filtered, r)
|
|
}
|
|
}
|
|
return filtered
|
|
}
|