174 lines
4.9 KiB
Go
174 lines
4.9 KiB
Go
package runtimeconfig
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"inbox/internal/base/timeutil"
|
|
"inbox/internal/domain/role"
|
|
"inbox/internal/domain/skill"
|
|
)
|
|
|
|
type Repository interface {
|
|
GetRole(ctx context.Context, name string) (role.Definition, error)
|
|
GetRoleConfig(ctx context.Context, roleName string) (role.Config, error)
|
|
ListRolePrompts(ctx context.Context, roleName string) ([]role.Prompt, error)
|
|
ListRoleSkillBindings(ctx context.Context, roleName string) ([]role.SkillBinding, error)
|
|
}
|
|
|
|
type SkillRepository interface {
|
|
ListSkillsByIDs(ctx context.Context, ids []string) (map[string]skill.Definition, error)
|
|
}
|
|
|
|
type ResolvedSkill struct {
|
|
Binding role.SkillBinding `json:"binding"`
|
|
Skill skill.Definition `json:"skill"`
|
|
}
|
|
|
|
type ResolvedRole struct {
|
|
Role role.Definition `json:"role"`
|
|
WorkspaceID string `json:"workspace_id,omitempty"`
|
|
Prompts map[role.PromptKind]role.Prompt `json:"prompts"`
|
|
Config role.Config `json:"config"`
|
|
Skills []ResolvedSkill `json:"skills"`
|
|
ResolvedAt string `json:"resolved_at"`
|
|
}
|
|
|
|
type Service struct {
|
|
repo Repository
|
|
skills SkillRepository
|
|
clock timeutil.Clock
|
|
}
|
|
|
|
func NewService(repo Repository, skills SkillRepository, clock timeutil.Clock) *Service {
|
|
if clock == nil {
|
|
clock = timeutil.SystemClock{}
|
|
}
|
|
return &Service{
|
|
repo: repo,
|
|
skills: skills,
|
|
clock: clock,
|
|
}
|
|
}
|
|
|
|
func (s *Service) ResolveRole(ctx context.Context, workspaceID, roleName string) (ResolvedRole, error) {
|
|
definition, err := s.repo.GetRole(ctx, roleName)
|
|
if err != nil {
|
|
return ResolvedRole{}, fmt.Errorf("get role %q: %w", roleName, err)
|
|
}
|
|
|
|
prompts, err := s.repo.ListRolePrompts(ctx, roleName)
|
|
if err != nil {
|
|
return ResolvedRole{}, fmt.Errorf("list role prompts for %q: %w", roleName, err)
|
|
}
|
|
bindings, err := s.repo.ListRoleSkillBindings(ctx, roleName)
|
|
if err != nil {
|
|
return ResolvedRole{}, fmt.Errorf("list role skill bindings for %q: %w", roleName, err)
|
|
}
|
|
|
|
resolved := ResolvedRole{
|
|
Role: definition,
|
|
WorkspaceID: workspaceID,
|
|
Prompts: pickPrompts(prompts, workspaceID),
|
|
ResolvedAt: timeutil.FormatRFC3339(s.clock.Now()),
|
|
}
|
|
config, err := s.repo.GetRoleConfig(ctx, roleName)
|
|
if err != nil {
|
|
return ResolvedRole{}, fmt.Errorf("get role config for %q: %w", roleName, err)
|
|
}
|
|
resolved.Config = config
|
|
resolved.Config.RoleName = roleName
|
|
|
|
resolvedBindings := pickBindings(bindings, workspaceID)
|
|
if len(resolvedBindings) == 0 {
|
|
return resolved, nil
|
|
}
|
|
|
|
skillIDs := make([]string, 0, len(resolvedBindings))
|
|
for _, binding := range resolvedBindings {
|
|
if binding.IsEnabled {
|
|
skillIDs = append(skillIDs, binding.SkillID)
|
|
}
|
|
}
|
|
skillsByID, err := s.skills.ListSkillsByIDs(ctx, skillIDs)
|
|
if err != nil {
|
|
return ResolvedRole{}, fmt.Errorf("list skills for %q: %w", roleName, err)
|
|
}
|
|
|
|
resolved.Skills = make([]ResolvedSkill, 0, len(skillIDs))
|
|
for _, binding := range resolvedBindings {
|
|
if !binding.IsEnabled {
|
|
continue
|
|
}
|
|
skillDef, ok := skillsByID[binding.SkillID]
|
|
if !ok {
|
|
return ResolvedRole{}, fmt.Errorf("missing skill %q for role %q", binding.SkillID, roleName)
|
|
}
|
|
resolved.Skills = append(resolved.Skills, ResolvedSkill{
|
|
Binding: binding,
|
|
Skill: skillDef,
|
|
})
|
|
}
|
|
sort.Slice(resolved.Skills, func(i, j int) bool {
|
|
if resolved.Skills[i].Binding.SortOrder == resolved.Skills[j].Binding.SortOrder {
|
|
if resolved.Skills[i].Skill.Name == resolved.Skills[j].Skill.Name {
|
|
return resolved.Skills[i].Skill.ID < resolved.Skills[j].Skill.ID
|
|
}
|
|
return resolved.Skills[i].Skill.Name < resolved.Skills[j].Skill.Name
|
|
}
|
|
return resolved.Skills[i].Binding.SortOrder < resolved.Skills[j].Binding.SortOrder
|
|
})
|
|
|
|
return resolved, nil
|
|
}
|
|
|
|
func (r ResolvedRole) SnapshotJSON() (string, error) {
|
|
data, err := json.Marshal(r)
|
|
if err != nil {
|
|
return "", fmt.Errorf("marshal runtime config snapshot: %w", err)
|
|
}
|
|
return string(data), nil
|
|
}
|
|
|
|
func pickPrompts(items []role.Prompt, workspaceID string) map[role.PromptKind]role.Prompt {
|
|
out := make(map[role.PromptKind]role.Prompt)
|
|
for _, item := range items {
|
|
if item.WorkspaceID != "" && item.WorkspaceID != workspaceID {
|
|
continue
|
|
}
|
|
current, exists := out[item.PromptKind]
|
|
if !exists {
|
|
out[item.PromptKind] = item
|
|
continue
|
|
}
|
|
if current.WorkspaceID == "" && item.WorkspaceID == workspaceID {
|
|
out[item.PromptKind] = item
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func pickBindings(items []role.SkillBinding, workspaceID string) []role.SkillBinding {
|
|
out := make(map[string]role.SkillBinding, len(items))
|
|
for _, item := range items {
|
|
if item.WorkspaceID != "" && item.WorkspaceID != workspaceID {
|
|
continue
|
|
}
|
|
current, exists := out[item.SkillID]
|
|
if !exists {
|
|
out[item.SkillID] = item
|
|
continue
|
|
}
|
|
if current.WorkspaceID == "" && item.WorkspaceID == workspaceID {
|
|
out[item.SkillID] = item
|
|
}
|
|
}
|
|
result := make([]role.SkillBinding, 0, len(out))
|
|
for _, item := range out {
|
|
result = append(result, item)
|
|
}
|
|
return result
|
|
}
|