tool loading: require explicit autoLoad, clean package structure
Remove lazy tool loading (load-on-call). Tools must now be explicitly listed in the agent's autoLoad config. Calling an unloaded tool fails with a clear error message. Package structure improvements: - embedding/index.go: generic Index type for semantic matching - skills/skills.go: uses embedding.Index internally, keeps Skill type - agent/skill_match.go: matchSkills() for skill discovery - agent/tool_match.go: matchTools() for tool hints (new file) Tool hints now match only loaded tools, not all tools on disk. This makes agent capabilities explicit and auditable.
This commit is contained in:
parent
263ea51d29
commit
fef6cdc307
|
|
@ -1,39 +1,22 @@
|
||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"ollie/embedding"
|
|
||||||
"ollie/skills"
|
"ollie/skills"
|
||||||
"ollie/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Skill matching configuration.
|
// Skill matching configuration.
|
||||||
const (
|
const (
|
||||||
// skillMatchThreshold is the minimum cosine similarity to include a skill.
|
|
||||||
skillMatchThreshold = 0.35
|
skillMatchThreshold = 0.35
|
||||||
// skillMatchLimit is the maximum number of skills to inject per turn.
|
|
||||||
skillMatchLimit = 3
|
skillMatchLimit = 3
|
||||||
|
|
||||||
// toolMatchThreshold is the minimum cosine similarity to include a tool hint.
|
|
||||||
toolMatchThreshold = 0.35
|
|
||||||
// toolMatchLimit is the maximum number of tool hints to inject per turn.
|
|
||||||
toolMatchLimit = 5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
skillIndex *skills.Index
|
skillIndex *skills.Index
|
||||||
skillInitErr error
|
skillInitErr error
|
||||||
skillOnce sync.Once
|
skillOnce sync.Once
|
||||||
|
|
||||||
toolIndex *skills.Index
|
|
||||||
toolInitErr error
|
|
||||||
toolOnce sync.Once
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// matchSkills finds skills relevant to the user's input and returns
|
// matchSkills finds skills relevant to the user's input and returns
|
||||||
|
|
@ -69,7 +52,6 @@ func matchSkills(input string) string {
|
||||||
sb.WriteString("\" relevance=\"")
|
sb.WriteString("\" relevance=\"")
|
||||||
sb.WriteString(formatScore(r.Score))
|
sb.WriteString(formatScore(r.Score))
|
||||||
sb.WriteString("\">\n")
|
sb.WriteString("\">\n")
|
||||||
// Include full skill content (already includes frontmatter)
|
|
||||||
sb.WriteString(r.Skill.Content)
|
sb.WriteString(r.Skill.Content)
|
||||||
if !strings.HasSuffix(r.Skill.Content, "\n") {
|
if !strings.HasSuffix(r.Skill.Content, "\n") {
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
|
|
@ -80,113 +62,10 @@ func matchSkills(input string) string {
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// matchTools finds tools relevant to the user's input and returns
|
|
||||||
// a hint block telling the model to call them.
|
|
||||||
func matchTools(input string) string {
|
|
||||||
toolOnce.Do(func() {
|
|
||||||
idx, err := newToolIndex()
|
|
||||||
if err != nil {
|
|
||||||
toolInitErr = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
toolIndex = idx
|
|
||||||
})
|
|
||||||
|
|
||||||
if toolIndex == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
workflowText := normalizeWorkflow(input).SearchText()
|
|
||||||
workflow := normalizeWorkflow(input)
|
|
||||||
searchInput := input
|
|
||||||
if workflowText != "" {
|
|
||||||
searchInput += " Workflow: " + workflowText
|
|
||||||
}
|
|
||||||
results, err := toolIndex.Match(searchInput, toolMatchThreshold, toolMatchLimit)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
for i := range results {
|
|
||||||
results[i].Score += workflowToolAdjustment(results[i].Skill.Name, workflow)
|
|
||||||
}
|
|
||||||
sort.SliceStable(results, func(i, j int) bool {
|
|
||||||
return results[i].Score > results[j].Score
|
|
||||||
})
|
|
||||||
if len(results) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb strings.Builder
|
|
||||||
sb.WriteString("<tool-hints>\n")
|
|
||||||
sb.WriteString("Relevant tools for this request:\n\n")
|
|
||||||
for _, r := range results {
|
|
||||||
sb.WriteString("→ `")
|
|
||||||
sb.WriteString(r.Skill.Name)
|
|
||||||
sb.WriteString("({...})` — ")
|
|
||||||
sb.WriteString(r.Skill.Description)
|
|
||||||
sb.WriteString("\n")
|
|
||||||
}
|
|
||||||
sb.WriteString("\nTo make one callable, use client_9p to load it through the agent ctl file:\n")
|
|
||||||
sb.WriteString("client_9p(op=\"rdwr\", path=\"session/$OLLIE_SESSION_ID/agent/$OLLIE_UNAME/ctl\", data=\"tool_load <name>\")\n")
|
|
||||||
sb.WriteString("Then call the loaded tool.\n")
|
|
||||||
sb.WriteString("</tool-hints>\n")
|
|
||||||
return sb.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatScore(s float32) string {
|
func formatScore(s float32) string {
|
||||||
// Format as percentage
|
|
||||||
pct := int(s * 100)
|
pct := int(s * 100)
|
||||||
if pct > 99 {
|
if pct > 99 {
|
||||||
pct = 99
|
pct = 99
|
||||||
}
|
}
|
||||||
return string([]byte{'0' + byte(pct/10), '0' + byte(pct%10), '%'})
|
return string([]byte{'0' + byte(pct/10), '0' + byte(pct%10), '%'})
|
||||||
}
|
}
|
||||||
|
|
||||||
// newToolIndex creates a skill-compatible index from tool .meta files.
|
|
||||||
func newToolIndex() (*skills.Index, error) {
|
|
||||||
model, err := embedding.LoadModel(skills.DefaultModelDir())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
toolsDir := filepath.Join(util.CfgDir(), "tools")
|
|
||||||
entries, err := os.ReadDir(toolsDir)
|
|
||||||
if err != nil {
|
|
||||||
model.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var toolSkills []skills.Skill
|
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".meta") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
metaPath := filepath.Join(toolsDir, entry.Name())
|
|
||||||
data, err := os.ReadFile(metaPath)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var meta struct {
|
|
||||||
Description string `json:"description"`
|
|
||||||
Prompt string `json:"prompt"`
|
|
||||||
Keywords []string `json:"keywords"`
|
|
||||||
}
|
|
||||||
if json.Unmarshal(data, &meta) != nil || meta.Description == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
toolName := strings.TrimSuffix(entry.Name(), ".meta")
|
|
||||||
searchText := meta.Description
|
|
||||||
if meta.Prompt != "" {
|
|
||||||
searchText += " Usage: " + meta.Prompt
|
|
||||||
}
|
|
||||||
if len(meta.Keywords) > 0 {
|
|
||||||
searchText += " Keywords: " + strings.Join(meta.Keywords, ", ")
|
|
||||||
}
|
|
||||||
toolSkills = append(toolSkills, skills.Skill{
|
|
||||||
Name: toolName,
|
|
||||||
Description: searchText,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return skills.NewIndexFromSkills(model, toolSkills)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"ollie/embedding"
|
||||||
|
"ollie/skills"
|
||||||
|
"ollie/toolsrv/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tool matching configuration.
|
||||||
|
const (
|
||||||
|
toolMatchThreshold = 0.35
|
||||||
|
toolMatchLimit = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Shared embedding model for tool matching.
|
||||||
|
toolEmbedModel *embedding.Model
|
||||||
|
toolEmbedErr error
|
||||||
|
toolEmbedOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// getToolEmbedModel returns the shared embedding model, loading it on first call.
|
||||||
|
func getToolEmbedModel() *embedding.Model {
|
||||||
|
toolEmbedOnce.Do(func() {
|
||||||
|
toolEmbedModel, toolEmbedErr = embedding.LoadModel(skills.DefaultModelDir())
|
||||||
|
})
|
||||||
|
return toolEmbedModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchTools finds tools relevant to the user's input from the loaded tools
|
||||||
|
// and returns a hint block telling the model to call them.
|
||||||
|
func matchTools(input string, tools []protocol.ToolInfo) string {
|
||||||
|
if len(tools) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
model := getToolEmbedModel()
|
||||||
|
if model == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert tools to embedding items
|
||||||
|
items := make([]embedding.Item, 0, len(tools))
|
||||||
|
for _, ti := range tools {
|
||||||
|
if ti.Description == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
searchText := ti.Description
|
||||||
|
if ti.Prompt != "" {
|
||||||
|
searchText += " Usage: " + ti.Prompt
|
||||||
|
}
|
||||||
|
items = append(items, embedding.Item{
|
||||||
|
Name: ti.Name,
|
||||||
|
Description: searchText,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build index and match
|
||||||
|
idx, err := embedding.NewIndex(model, items)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
workflowText := normalizeWorkflow(input).SearchText()
|
||||||
|
workflow := normalizeWorkflow(input)
|
||||||
|
searchInput := input
|
||||||
|
if workflowText != "" {
|
||||||
|
searchInput += " Workflow: " + workflowText
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := idx.Match(searchInput, toolMatchThreshold, toolMatchLimit)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply workflow adjustments
|
||||||
|
for i := range results {
|
||||||
|
results[i].Score += workflowToolAdjustment(results[i].Item.Name, workflow)
|
||||||
|
}
|
||||||
|
sort.SliceStable(results, func(i, j int) bool {
|
||||||
|
return results[i].Score > results[j].Score
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(results) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("<tool-hints>\n")
|
||||||
|
sb.WriteString("Relevant tools for this request:\n\n")
|
||||||
|
for _, r := range results {
|
||||||
|
sb.WriteString("→ `")
|
||||||
|
sb.WriteString(r.Item.Name)
|
||||||
|
sb.WriteString("({...})` — ")
|
||||||
|
sb.WriteString(r.Item.Description)
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
sb.WriteString("</tool-hints>\n")
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
@ -131,8 +131,13 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string {
|
||||||
if skillContent := matchSkills(input); skillContent != "" {
|
if skillContent := matchSkills(input); skillContent != "" {
|
||||||
contextParts = append(contextParts, skillContent)
|
contextParts = append(contextParts, skillContent)
|
||||||
}
|
}
|
||||||
if ag.runtime.ToolServer != nil {
|
if len(ag.runtime.ToolMeta) > 0 {
|
||||||
if toolHints := matchTools(input); toolHints != "" {
|
// Convert map to slice for matching
|
||||||
|
tools := make([]protocol.ToolInfo, 0, len(ag.runtime.ToolMeta))
|
||||||
|
for _, ti := range ag.runtime.ToolMeta {
|
||||||
|
tools = append(tools, ti)
|
||||||
|
}
|
||||||
|
if toolHints := matchTools(input, tools); toolHints != "" {
|
||||||
contextParts = append(contextParts, toolHints)
|
contextParts = append(contextParts, toolHints)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -290,14 +290,7 @@ func (st *State) NewProc(ctx context.Context, payload string, background bool) (
|
||||||
|
|
||||||
info, ok := reg.Lookup(aid, toolName)
|
info, ok := reg.Lookup(aid, toolName)
|
||||||
if !ok {
|
if !ok {
|
||||||
// Auto-load if tool exists
|
return "", 0, fmt.Errorf("tool not loaded: %s (add to autoLoad in agent config)", toolName)
|
||||||
if err := reg.Load(aid, toolName); err != nil {
|
|
||||||
return "", 0, err // "tool not found: X"
|
|
||||||
}
|
|
||||||
info, _ = reg.Lookup(aid, toolName)
|
|
||||||
if st.OnToolsChanged != nil {
|
|
||||||
st.OnToolsChanged()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reject shell calls that invoke a native tool.
|
// Reject shell calls that invoke a native tool.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
package embedding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Item represents something that can be embedded and matched.
|
||||||
|
type Item struct {
|
||||||
|
Name string
|
||||||
|
Description string // text used for embedding
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index holds precomputed embeddings for semantic matching.
|
||||||
|
type Index struct {
|
||||||
|
model *Model
|
||||||
|
items []Item
|
||||||
|
vecs []Vector
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIndex creates an index from items using the given embedding model.
|
||||||
|
// The model is borrowed, not owned — caller is responsible for its lifecycle.
|
||||||
|
func NewIndex(model *Model, items []Item) (*Index, error) {
|
||||||
|
if model == nil {
|
||||||
|
return nil, fmt.Errorf("model is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
vecs := make([]Vector, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
vec, err := model.Embed(item.Description)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("embed %q: %w", item.Name, err)
|
||||||
|
}
|
||||||
|
vecs[i] = vec
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Index{
|
||||||
|
model: model,
|
||||||
|
items: items,
|
||||||
|
vecs: vecs,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match returns items matching the query, sorted by relevance.
|
||||||
|
// threshold is the minimum cosine similarity (0-1) to include.
|
||||||
|
// limit is the maximum number of results (0 for no limit).
|
||||||
|
func (idx *Index) Match(query string, threshold float32, limit int) ([]MatchResult, error) {
|
||||||
|
idx.mu.RLock()
|
||||||
|
defer idx.mu.RUnlock()
|
||||||
|
|
||||||
|
if len(idx.items) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
qvec, err := idx.model.Embed(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("embed query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []MatchResult
|
||||||
|
for i, item := range idx.items {
|
||||||
|
sim := CosineSimilarity(qvec, idx.vecs[i])
|
||||||
|
if sim >= threshold {
|
||||||
|
results = append(results, MatchResult{
|
||||||
|
Item: item,
|
||||||
|
Score: sim,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(results, func(i, j int) bool {
|
||||||
|
return results[i].Score > results[j].Score
|
||||||
|
})
|
||||||
|
|
||||||
|
if limit > 0 && len(results) > limit {
|
||||||
|
results = results[:limit]
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchResult holds a matched item and its similarity score.
|
||||||
|
type MatchResult struct {
|
||||||
|
Item Item
|
||||||
|
Score float32
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// Package skills provides skill discovery, embedding, and matching.
|
// Package skills provides skill discovery and matching.
|
||||||
|
// Skills are markdown knowledge modules with YAML frontmatter.
|
||||||
package skills
|
package skills
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -7,7 +8,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
|
@ -27,10 +27,16 @@ type Skill struct {
|
||||||
type Index struct {
|
type Index struct {
|
||||||
model *embedding.Model
|
model *embedding.Model
|
||||||
skills []Skill
|
skills []Skill
|
||||||
vecs []embedding.Vector
|
index *embedding.Index
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MatchResult holds a matched skill and its similarity score.
|
||||||
|
type MatchResult struct {
|
||||||
|
Skill Skill
|
||||||
|
Score float32
|
||||||
|
}
|
||||||
|
|
||||||
// NewIndex creates a new skill index with precomputed embeddings.
|
// NewIndex creates a new skill index with precomputed embeddings.
|
||||||
// modelDir is the path to the directory containing the ONNX model.
|
// modelDir is the path to the directory containing the ONNX model.
|
||||||
// skillDirs is a list of directories to scan for skills.
|
// skillDirs is a list of directories to scan for skills.
|
||||||
|
|
@ -48,25 +54,6 @@ func NewIndex(modelDir string, skillDirs []string) (*Index, error) {
|
||||||
return idx, nil
|
return idx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewIndexFromSkills creates an index from pre-loaded skills.
|
|
||||||
// Takes ownership of the model.
|
|
||||||
func NewIndexFromSkills(model *embedding.Model, skills []Skill) (*Index, error) {
|
|
||||||
idx := &Index{model: model, skills: skills}
|
|
||||||
|
|
||||||
// Compute embeddings
|
|
||||||
vecs := make([]embedding.Vector, len(skills))
|
|
||||||
for i, skill := range skills {
|
|
||||||
vec, err := model.Embed(skill.Description)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("embed skill %q: %w", skill.Name, err)
|
|
||||||
}
|
|
||||||
vecs[i] = vec
|
|
||||||
}
|
|
||||||
idx.vecs = vecs
|
|
||||||
|
|
||||||
return idx, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close releases resources.
|
// Close releases resources.
|
||||||
func (idx *Index) Close() error {
|
func (idx *Index) Close() error {
|
||||||
if idx.model != nil {
|
if idx.model != nil {
|
||||||
|
|
@ -82,41 +69,27 @@ func (idx *Index) Match(query string, threshold float32, limit int) ([]MatchResu
|
||||||
idx.mu.RLock()
|
idx.mu.RLock()
|
||||||
defer idx.mu.RUnlock()
|
defer idx.mu.RUnlock()
|
||||||
|
|
||||||
if len(idx.skills) == 0 {
|
if idx.index == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
qvec, err := idx.model.Embed(query)
|
results, err := idx.index.Match(query, threshold, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("embed query: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []MatchResult
|
// Convert embedding.MatchResult to MatchResult with full Skill
|
||||||
for i, skill := range idx.skills {
|
out := make([]MatchResult, len(results))
|
||||||
sim := embedding.CosineSimilarity(qvec, idx.vecs[i])
|
for i, r := range results {
|
||||||
if sim >= threshold {
|
// Find the skill by name
|
||||||
results = append(results, MatchResult{
|
for _, skill := range idx.skills {
|
||||||
Skill: skill,
|
if skill.Name == r.Item.Name {
|
||||||
Score: sim,
|
out[i] = MatchResult{Skill: skill, Score: r.Score}
|
||||||
})
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by score descending
|
|
||||||
sort.Slice(results, func(i, j int) bool {
|
|
||||||
return results[i].Score > results[j].Score
|
|
||||||
})
|
|
||||||
|
|
||||||
if limit > 0 && len(results) > limit {
|
|
||||||
results = results[:limit]
|
|
||||||
}
|
}
|
||||||
return results, nil
|
return out, nil
|
||||||
}
|
|
||||||
|
|
||||||
// MatchResult holds a matched skill and its similarity score.
|
|
||||||
type MatchResult struct {
|
|
||||||
Skill Skill
|
|
||||||
Score float32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// All returns all indexed skills.
|
// All returns all indexed skills.
|
||||||
|
|
@ -167,18 +140,23 @@ func (idx *Index) loadSkillsLocked(dirs []string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute embeddings
|
// Convert to embedding.Item for the generic index
|
||||||
vecs := make([]embedding.Vector, len(skills))
|
items := make([]embedding.Item, len(skills))
|
||||||
for i, skill := range skills {
|
for i, skill := range skills {
|
||||||
vec, err := idx.model.Embed(skill.Description)
|
items[i] = embedding.Item{
|
||||||
if err != nil {
|
Name: skill.Name,
|
||||||
return fmt.Errorf("embed skill %q: %w", skill.Name, err)
|
Description: skill.Description,
|
||||||
}
|
}
|
||||||
vecs[i] = vec
|
}
|
||||||
|
|
||||||
|
// Build embedding index
|
||||||
|
embIdx, err := embedding.NewIndex(idx.model, items)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
idx.skills = skills
|
idx.skills = skills
|
||||||
idx.vecs = vecs
|
idx.index = embIdx
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue