mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-07 21:13:27 -05:00
The Memory add-on only declared three index files, two skills and a save setting; the host did the rest. Without the add-on a memory proposal worked but nothing was read back, and removing the add-on silently reset the save level. The host now loads the three indexes (user, agent, conversation) with the same caps and data wrapping, serves context_read, ships the two skills as built-in skills with the same text, and keeps the save level as its own per-person setting (ask, auto, never; default ask). The code sits behind a small MemoryProvider interface. The add-on stays installed and harmless: its index files and skills are ignored while the host serves memory, so nothing loads twice. A person who chose a level on its page keeps it until they choose in the host setting; auto still needs the add-on to run. A person who disabled the add-on and never chose keeps memory off. Saved agents still read notes only when their add-ons list names memory, and messaging chats still ask before saving. A person with no note and no add-on gets no extra block in the prompt. Assisted-by: Claude:claude-sonnet-5-5
162 lines
5.9 KiB
Go
162 lines
5.9 KiB
Go
// SPDX-License-Identifier: MIT
|
|
package contract
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// MaxSkillDefinitions bounds the skills one add-on ships. The chat's skill
|
|
// index has room for 100 lines and 16 KiB; one add-on must not fill it.
|
|
const MaxSkillDefinitions = 16
|
|
|
|
// SkillsDir is the folder of a package that holds one folder per skill:
|
|
// skills/<name>/SKILL.md plus optional reference files.
|
|
const SkillsDir = "skills"
|
|
|
|
// SkillDefinition declares one skill the package ships under
|
|
// skills/<Name>/. The description is not here: the SKILL.md frontmatter is
|
|
// its one source.
|
|
type SkillDefinition struct {
|
|
// Name is the skill's name and the name of its folder. A plain skill is
|
|
// named after the add-on: the add-on ID, or the ID and a dash and more
|
|
// (office-quickstart), so two add-ons never ship one plain name. A skill
|
|
// for a reserved document slot has the slot's name (see Shadows).
|
|
Name string `json:"name"`
|
|
// Shadows names the document-provider slot this skill supplies. It must equal Name
|
|
// and name a slot: one the skill declares (Slot) or a default one
|
|
// (DefaultSlots). It repeats the name on purpose: the
|
|
// replacement is an explicit act, which the install warning and the check
|
|
// read from the manifest without opening the package. Empty: a plain
|
|
// add-on skill.
|
|
Shadows string `json:"shadows,omitempty"`
|
|
// Slot describes the slot this skill serves: the files it covers and the
|
|
// words to say about them (see SlotDefinition). It needs Shadows. A skill that shadows a default slot may leave it out.
|
|
Slot *SlotDefinition `json:"slot,omitempty"`
|
|
}
|
|
|
|
// protectedSkills are the built-in skills that no add-on may replace or
|
|
// ship under their name: they describe LocalAI itself. A test derives the
|
|
// built-in names from the embedded tree and fails when one is neither here
|
|
// nor a reserved document slot.
|
|
var protectedSkills = []string{"localai-workspace", "localai-apps", "writing-skills", "memory", "memory-check-own-files"}
|
|
|
|
// LegacyMemoryAddonID is the add-on that used to ship the two memory skills.
|
|
// They are built into the host now; the package keeps declaring them so an
|
|
// installed copy stays valid, and the host does not load its copies (the
|
|
// built-in skills win).
|
|
const LegacyMemoryAddonID = "memory"
|
|
|
|
// LegacyMemorySkill reports whether name is one of the memory skills that the
|
|
// add-on id declared before they became built-in.
|
|
func LegacyMemorySkill(addonID, name string) bool {
|
|
return addonID == LegacyMemoryAddonID && (name == "memory" || name == "memory-check-own-files")
|
|
}
|
|
|
|
// ProtectedSkill reports whether name is a built-in skill that an add-on may
|
|
// not replace.
|
|
func ProtectedSkill(name string) bool {
|
|
return slices.Contains(protectedSkills, name)
|
|
}
|
|
|
|
// DocumentSkill reports whether name is a default document slot (slots.json).
|
|
// An add-on can declare more slots of its own; the host learns them from the
|
|
// skills it lists, not from here.
|
|
func DocumentSkill(name string) bool {
|
|
s, ok := DefaultSlot(name)
|
|
return ok && s.Kind == SlotKindDocuments
|
|
}
|
|
|
|
// ReplaceableSkills lists the default slots.
|
|
var ReplaceableSkills = func() []string {
|
|
var out []string
|
|
for _, s := range defaultSlots() {
|
|
out = append(out, s.Name)
|
|
}
|
|
return out
|
|
}()
|
|
|
|
// ReplaceableSkill is the compatibility name for DocumentSkill.
|
|
func ReplaceableSkill(name string) bool { return DocumentSkill(name) }
|
|
|
|
// BuiltinSkillName reports the protected core skill names, not provider slots.
|
|
func BuiltinSkillName(name string) bool { return ProtectedSkill(name) }
|
|
|
|
// PlainSkillName reports whether name is a plain skill name the add-on id may
|
|
// ship: the id itself, or the id followed by a dash and more.
|
|
func PlainSkillName(addonID, name string) bool {
|
|
return name == addonID || strings.HasPrefix(name, addonID+"-")
|
|
}
|
|
|
|
// validateSkillDefinitions checks the skills of the add-on id: bounded, well
|
|
// formed, unique, and named after the add-on.
|
|
func validateSkillDefinitions(id string, defs []SkillDefinition) error {
|
|
if len(defs) == 0 {
|
|
return nil
|
|
}
|
|
if len(defs) > MaxSkillDefinitions {
|
|
return fmt.Errorf("too many skill definitions: at most %d", MaxSkillDefinitions)
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, d := range defs {
|
|
if !namespace.MatchString(d.Name) {
|
|
return fmt.Errorf("skill %q: not a valid skill name", d.Name)
|
|
}
|
|
if seen[d.Name] {
|
|
return fmt.Errorf("skill %s: declared twice", d.Name)
|
|
}
|
|
seen[d.Name] = true
|
|
if d.Shadows != "" {
|
|
switch {
|
|
case d.Shadows != d.Name:
|
|
return fmt.Errorf("skill %s: shadows must repeat the skill's name", d.Name)
|
|
case ProtectedSkill(d.Shadows):
|
|
return fmt.Errorf("skill %s: %q is a built-in skill that an add-on may replace in no way", d.Name, d.Shadows)
|
|
case d.Slot == nil && !ReplaceableSkill(d.Shadows):
|
|
return fmt.Errorf("skill %s: %q is not a slot an add-on may replace; declare its slot", d.Name, d.Shadows)
|
|
}
|
|
if d.Slot != nil {
|
|
if err := d.Slot.validate(d.Name); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
if d.Slot != nil {
|
|
return fmt.Errorf("skill %s: a slot needs shadows", d.Name)
|
|
}
|
|
if DocumentSkill(d.Name) {
|
|
return fmt.Errorf("skill %s: reserved document name requires same-name shadows", d.Name)
|
|
}
|
|
if BuiltinSkillName(d.Name) && !LegacyMemorySkill(id, d.Name) {
|
|
return fmt.Errorf("skill %s: the name is a built-in skill", d.Name)
|
|
}
|
|
if !PlainSkillName(id, d.Name) {
|
|
return fmt.Errorf("skill %s: a skill name must be %s or start with %s-", d.Name, id, id)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ShadowedSkills lists, in definition order, the document slots supplied by
|
|
// the skill definitions.
|
|
func ShadowedSkills(defs []SkillDefinition) []string {
|
|
var out []string
|
|
for _, d := range defs {
|
|
if _, ok := d.slot(); ok {
|
|
out = append(out, d.Shadows)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// SkillNames lists the names of the skill definitions, in order.
|
|
func SkillNames(defs []SkillDefinition) []string {
|
|
out := make([]string, 0, len(defs))
|
|
for _, d := range defs {
|
|
out = append(out, d.Name)
|
|
}
|
|
return out
|
|
}
|