package service import ( "context" "fmt" "os" "path/filepath" "sync" "github.com/Tencent/WeKnora/internal/agent/skills" "github.com/Tencent/WeKnora/internal/logger" "github.com/Tencent/WeKnora/internal/types/interfaces" ) // DefaultPreloadedSkillsDir is the default directory for preloaded skills const DefaultPreloadedSkillsDir = "skills/preloaded" // skillService implements SkillService interface type skillService struct { loader *skills.Loader preloadedDir string mu sync.RWMutex initialized bool } // NewSkillService creates a new skill service func NewSkillService() interfaces.SkillService { // Determine the preloaded skills directory preloadedDir := getPreloadedSkillsDir() return &skillService{ preloadedDir: preloadedDir, initialized: false, } } // getPreloadedSkillsDir returns the path to the preloaded skills directory func getPreloadedSkillsDir() string { // Check if SKILLS_DIR environment variable is set if dir := os.Getenv("WEKNORA_SKILLS_DIR"); dir != "" { return dir } // Try to find the skills directory relative to the executable execPath, err := os.Executable() if err == nil { execDir := filepath.Dir(execPath) skillsDir := filepath.Join(execDir, DefaultPreloadedSkillsDir) if _, err := os.Stat(skillsDir); err == nil { return skillsDir } } // Try current working directory cwd, err := os.Getwd() if err == nil { skillsDir := filepath.Join(cwd, DefaultPreloadedSkillsDir) if _, err := os.Stat(skillsDir); err == nil { return skillsDir } } // Default to relative path (will be created if needed) return DefaultPreloadedSkillsDir } // ensureInitialized initializes the loader if not already done func (s *skillService) ensureInitialized(ctx context.Context) error { s.mu.Lock() defer s.mu.Unlock() if s.initialized { return nil } // Check if preloaded directory exists if _, err := os.Stat(s.preloadedDir); os.IsNotExist(err) { logger.Warnf(ctx, "Preloaded skills directory does not exist: %s", s.preloadedDir) // Create the directory to avoid repeated warnings if err := os.MkdirAll(s.preloadedDir, 0755); err != nil { logger.Warnf(ctx, "Failed to create preloaded skills directory: %v", err) } } // Create loader with preloaded directory s.loader = skills.NewLoader([]string{s.preloadedDir}) s.initialized = true logger.Infof(ctx, "Skill service initialized with preloaded directory: %s", s.preloadedDir) return nil } // ListPreloadedSkills returns metadata for all preloaded skills func (s *skillService) ListPreloadedSkills(ctx context.Context) ([]*skills.SkillMetadata, error) { if err := s.ensureInitialized(ctx); err != nil { return nil, fmt.Errorf("failed to initialize skill service: %w", err) } s.mu.RLock() defer s.mu.RUnlock() metadata, err := s.loader.DiscoverSkills() if err != nil { logger.Errorf(ctx, "Failed to discover preloaded skills: %v", err) return nil, fmt.Errorf("failed to discover skills: %w", err) } logger.Infof(ctx, "Discovered %d preloaded skills", len(metadata)) return metadata, nil } // GetSkillByName retrieves a skill by its name func (s *skillService) GetSkillByName(ctx context.Context, name string) (*skills.Skill, error) { if err := s.ensureInitialized(ctx); err != nil { return nil, fmt.Errorf("failed to initialize skill service: %w", err) } s.mu.RLock() defer s.mu.RUnlock() skill, err := s.loader.LoadSkillInstructions(name) if err != nil { logger.Errorf(ctx, "Failed to load skill %s: %v", name, err) return nil, fmt.Errorf("failed to load skill: %w", err) } return skill, nil } // GetPreloadedDir returns the configured preloaded skills directory func (s *skillService) GetPreloadedDir() string { return s.preloadedDir }