feat(ai): add local expert library
This commit is contained in:
26
backend/internal/models/ai_expert.go
Normal file
26
backend/internal/models/ai_expert.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// SenlinAgentAIExpert 是本地专家库中的可选 AI 角色。
|
||||
type SenlinAgentAIExpert struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
Slug string `gorm:"size:160;uniqueIndex;not null"`
|
||||
Category string `gorm:"size:80;index;not null"`
|
||||
CategoryName string `gorm:"size:120;not null"`
|
||||
Name string `gorm:"size:160;index;not null"`
|
||||
Description string `gorm:"type:text;not null"`
|
||||
Emoji string `gorm:"size:32"`
|
||||
Color string `gorm:"size:32"`
|
||||
SystemPrompt string `gorm:"type:text;not null"`
|
||||
Source string `gorm:"size:255;not null"`
|
||||
SourceLicense string `gorm:"size:32;not null"`
|
||||
Enabled bool `gorm:"not null;default:true;index"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (SenlinAgentAIExpert) TableName() string {
|
||||
return "senlin_agent_ai_experts"
|
||||
}
|
||||
@@ -3,15 +3,18 @@ package models
|
||||
import "time"
|
||||
|
||||
type SenlinAgentAISession struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
ProjectID uint `gorm:"index;not null"`
|
||||
ProjectIdentity string `gorm:"type:char(36);index"`
|
||||
CreatedBy uint `gorm:"index;not null"`
|
||||
CreatedByIdentity string `gorm:"type:char(36);index"`
|
||||
Title string `gorm:"not null"`
|
||||
Context string `gorm:"type:text"`
|
||||
Status string `gorm:"not null;default:ready"`
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
ProjectID uint `gorm:"index;not null"`
|
||||
ProjectIdentity string `gorm:"type:char(36);index"`
|
||||
CreatedBy uint `gorm:"index;not null"`
|
||||
CreatedByIdentity string `gorm:"type:char(36);index"`
|
||||
ExpertID *uint `gorm:"index"`
|
||||
ExpertIdentity *string `gorm:"type:char(36);index"`
|
||||
Expert *SenlinAgentAIExpert `gorm:"foreignKey:ExpertID"`
|
||||
Title string `gorm:"not null"`
|
||||
Context string `gorm:"type:text"`
|
||||
Status string `gorm:"not null;default:ready"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
2677
backend/internal/models/experts_catalog.json
Normal file
2677
backend/internal/models/experts_catalog.json
Normal file
File diff suppressed because one or more lines are too long
@@ -90,7 +90,14 @@ func (m *SenlinAgentAISession) BeforeCreate(tx *gorm.DB) error {
|
||||
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
return resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity)
|
||||
if err := resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
return resolveOptionalIdentity(tx, &SenlinAgentAIExpert{}, m.ExpertID, &m.ExpertIdentity)
|
||||
}
|
||||
|
||||
func (m *SenlinAgentAIExpert) BeforeCreate(_ *gorm.DB) error {
|
||||
return ensureIdentity(&m.Identity)
|
||||
}
|
||||
|
||||
func (m *SenlinAgentTag) BeforeCreate(tx *gorm.DB) error {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -31,6 +32,7 @@ func runVersionedMigrations(database *gorm.DB) error {
|
||||
migrations := []versionedMigration{
|
||||
{version: 1, name: "normalize_project_identifiers", run: normalizeLegacyProjectIdentifiers},
|
||||
{version: 2, name: "deduplicate_project_tags", run: deduplicateLegacyProjectTags},
|
||||
{version: 3, name: "seed_ai_experts", run: seedAIExperts},
|
||||
}
|
||||
for _, migration := range migrations {
|
||||
// Connection callbacks can provide an initialized Gorm session. Start each
|
||||
@@ -63,6 +65,48 @@ func runVersionedMigrations(database *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:embed experts_catalog.json
|
||||
var expertCatalogJSON []byte
|
||||
|
||||
type embeddedExpertCatalog struct {
|
||||
Source string `json:"source"`
|
||||
License string `json:"license"`
|
||||
Experts []embeddedExpertCatalogItem `json:"experts"`
|
||||
}
|
||||
|
||||
type embeddedExpertCatalogItem struct {
|
||||
Slug string `json:"slug"`
|
||||
Category string `json:"category"`
|
||||
CategoryName string `json:"categoryName"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Emoji string `json:"emoji"`
|
||||
Color string `json:"color"`
|
||||
SystemPrompt string `json:"systemPrompt"`
|
||||
}
|
||||
|
||||
func seedAIExperts(tx *gorm.DB) (map[string]int, error) {
|
||||
var catalog embeddedExpertCatalog
|
||||
if err := json.Unmarshal(expertCatalogJSON, &catalog); err != nil {
|
||||
return nil, fmt.Errorf("decode embedded AI expert catalog: %w", err)
|
||||
}
|
||||
records := make([]SenlinAgentAIExpert, 0, len(catalog.Experts))
|
||||
for _, expert := range catalog.Experts {
|
||||
records = append(records, SenlinAgentAIExpert{
|
||||
Slug: expert.Slug, Category: expert.Category, CategoryName: expert.CategoryName,
|
||||
Name: expert.Name, Description: expert.Description, Emoji: expert.Emoji,
|
||||
Color: expert.Color, SystemPrompt: expert.SystemPrompt, Source: catalog.Source,
|
||||
SourceLicense: catalog.License, Enabled: true,
|
||||
})
|
||||
}
|
||||
if len(records) > 0 {
|
||||
if err := tx.CreateInBatches(&records, 25).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return map[string]int{"inserted": len(records)}, nil
|
||||
}
|
||||
|
||||
func normalizeLegacyProjectIdentifiers(tx *gorm.DB) (map[string]int, error) {
|
||||
var projects []SenlinAgentProject
|
||||
if err := tx.Model(&SenlinAgentProject{}).Select("id", "owner_id", "identifier").Order("owner_id asc, id asc").Find(&projects).Error; err != nil {
|
||||
|
||||
@@ -57,14 +57,18 @@ func TestAutoMigrateUpgradesLegacyProjectsAndTagsWithoutLosingAssociations(t *te
|
||||
|
||||
var migrationCount int64
|
||||
require.NoError(t, database.Table("senlin_agent_schema_migrations").Count(&migrationCount).Error)
|
||||
require.Equal(t, int64(2), migrationCount)
|
||||
require.Equal(t, int64(3), migrationCount)
|
||||
var auditDetails []string
|
||||
require.NoError(t, database.Table("senlin_agent_schema_migrations").Order("version asc").Pluck("details", &auditDetails).Error)
|
||||
require.Contains(t, auditDetails[0], `"updated":3`)
|
||||
require.Contains(t, auditDetails[1], `"deduplicated":1`)
|
||||
require.Contains(t, auditDetails[2], `"inserted":267`)
|
||||
var expertCount int64
|
||||
require.NoError(t, database.Model(&SenlinAgentAIExpert{}).Count(&expertCount).Error)
|
||||
require.Equal(t, int64(267), expertCount)
|
||||
require.NoError(t, AutoMigrate(database), "versioned migrations must be safe to run again")
|
||||
require.NoError(t, database.Table("senlin_agent_schema_migrations").Count(&migrationCount).Error)
|
||||
require.Equal(t, int64(2), migrationCount)
|
||||
require.Equal(t, int64(3), migrationCount)
|
||||
|
||||
require.Error(t, database.Exec(`INSERT INTO senlin_agent_projects (owner_id, name, identifier) VALUES (7, 'still duplicate', 'DUP')`).Error)
|
||||
require.Error(t, database.Exec(`INSERT INTO senlin_agent_tags (project_id, name) VALUES (1, 'UI')`).Error)
|
||||
|
||||
@@ -32,6 +32,7 @@ func AutoMigrate(database *gorm.DB) error {
|
||||
&SenlinAgentTask{},
|
||||
&SenlinAgentNote{},
|
||||
&SenlinAgentSource{},
|
||||
&SenlinAgentAIExpert{},
|
||||
&SenlinAgentAISession{},
|
||||
&SenlinAgentTag{},
|
||||
&SenlinAgentProjectChannel{},
|
||||
|
||||
Reference in New Issue
Block a user