Files
LocalAI/core/services/distributed/skills.go
Ettore Di Giacinto 59108fbe32 feat: add distributed mode (#9124)
* feat: add distributed mode (experimental)

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix data races, mutexes, transactions

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactorings

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fixups

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix events and tool stream in agent chat

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* use ginkgo

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactoring and consolidation

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactoring and consolidation

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactoring and consolidation

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactoring and consolidation

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactoring and consolidation

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactoring and consolidation

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactoring and consolidation

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactoring and consolidation

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(cron): compute correctly time boundaries avoiding re-triggering

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* enhancements, refactorings

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* do not flood of healthy checks

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* do not list obvious backends as text backends

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* tests fixups

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactoring and consolidation

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Drop redundant healthcheck

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* enhancements, refactorings

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-30 00:47:27 +02:00

97 lines
2.9 KiB
Go

package distributed
import (
"fmt"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// SkillMetadataRecord tracks skill metadata in PostgreSQL.
type SkillMetadataRecord struct {
ID string `gorm:"primaryKey;size:36" json:"id"`
UserID string `gorm:"index;size:36" json:"user_id,omitempty"`
Name string `gorm:"index;size:255" json:"name"`
Definition string `gorm:"type:text" json:"definition,omitempty"` // SKILL.md content or YAML
SourceType string `gorm:"size:32" json:"source_type"` // "inline", "git"
SourceURL string `gorm:"size:512" json:"source_url,omitempty"`
Version string `gorm:"size:64" json:"version,omitempty"`
Enabled bool `gorm:"default:true" json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (SkillMetadataRecord) TableName() string { return "skills_metadata" }
// SkillStore manages skill metadata in PostgreSQL.
type SkillStore struct {
db *gorm.DB
}
// NewSkillStore creates a new SkillStore and auto-migrates.
func NewSkillStore(db *gorm.DB) (*SkillStore, error) {
if err := db.AutoMigrate(&SkillMetadataRecord{}); err != nil {
return nil, fmt.Errorf("migrating skills_metadata: %w", err)
}
return &SkillStore{db: db}, nil
}
// Save creates or updates a skill metadata record.
func (s *SkillStore) Save(rec *SkillMetadataRecord) error {
if rec.ID == "" {
rec.ID = uuid.New().String()
}
rec.UpdatedAt = time.Now()
if rec.CreatedAt.IsZero() {
rec.CreatedAt = rec.UpdatedAt
}
var existing SkillMetadataRecord
err := s.db.Where("user_id = ? AND name = ?", rec.UserID, rec.Name).First(&existing).Error
if err == nil {
rec.ID = existing.ID
rec.CreatedAt = existing.CreatedAt
return s.db.Model(&existing).Updates(rec).Error
}
return s.db.Create(rec).Error
}
// Get retrieves a skill by user and name.
func (s *SkillStore) Get(userID, name string) (*SkillMetadataRecord, error) {
var rec SkillMetadataRecord
q := s.db.Where("name = ?", name)
if userID != "" {
q = q.Where("user_id = ?", userID)
}
if err := q.First(&rec).Error; err != nil {
return nil, err
}
return &rec, nil
}
// List returns skills for a user.
func (s *SkillStore) List(userID string) ([]SkillMetadataRecord, error) {
var recs []SkillMetadataRecord
q := s.db.Order("name")
if userID != "" {
q = q.Where("user_id = ?", userID)
}
return recs, q.Find(&recs).Error
}
// Delete removes a skill metadata record.
func (s *SkillStore) Delete(userID, name string) error {
q := s.db.Where("name = ?", name)
if userID != "" {
q = q.Where("user_id = ?", userID)
}
return q.Delete(&SkillMetadataRecord{}).Error
}
// ListGitSkills returns skills sourced from git repos (for sync jobs).
func (s *SkillStore) ListGitSkills() ([]SkillMetadataRecord, error) {
var recs []SkillMetadataRecord
return recs, s.db.Where("source_type = ? AND enabled = true", "git").Find(&recs).Error
}