Files
LocalAI/core/services/studio/binaryhint.go
T
Ettore Di Giacinto 337a034601 feat(addons): declare document slots and legacy arguments in the manifest
The four document skill names were code in the contract, the hint for
binary files kept its own list of formats and libraries, and the chat
boundary rewrote the old arguments of one add-on by its ID.

A skill that shadows a name can now declare the slot it serves: kind,
file extensions, name words and a library hint. A tool can declare
legacyArgs, the arguments it refuses now, each with a hint. Both need
manifest api 2. The four default slots move to a data file that the
contract reads, so a manifest that declares nothing keeps working.

The host takes the replaceable names, the files a skill covers and the
hint for a script from the skills the add-ons list. It compares no
format or add-on ID. A guard spec fails when it does again. The Office
manifest declares its slots and legacy arguments and accepts api 2.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt
2026-11-03 14:45:43 +00:00

91 lines
3.0 KiB
Go

package studio
import (
"context"
"encoding/json"
"errors"
"fmt"
"path"
"slices"
"strings"
"github.com/mudler/LocalAI/core/services/studio/files"
"github.com/mudler/LocalAI/core/services/studio/session"
"github.com/mudler/LocalAI/core/services/studio/workspace"
"github.com/mudler/LocalAI/pkg/addons/contract"
)
// binaryHintTools wraps the tools that read a file as text: when one meets a
// binary file, the error says what the chat can do with it. It names the
// skill that matches the file type when the chat has one, and a script when
// the chat can run commands. The skills are resolved only when a read fails.
func (s *Service) binaryHintTools(a workspace.Actor, canRun bool, tools []session.Tool) []session.Tool {
for i, tool := range tools {
switch tool.Name {
case "read_file", "edit_file":
default:
continue
}
run := tool.Run
tools[i].Run = func(ctx context.Context, args json.RawMessage) (string, error) {
out, err := run(ctx, args)
var nt *files.NotTextError
if !errors.As(err, &nt) {
return out, err
}
return out, errors.New(s.binaryAdvice(ctx, a, canRun, nt))
}
}
return tools
}
// binaryAdvice is the error for a binary file, short and true for the tools
// the chat has.
func (s *Service) binaryAdvice(ctx context.Context, a workspace.Actor, canRun bool, nt *files.NotTextError) string {
ext := strings.ToLower(path.Ext(nt.Name))
msg := fmt.Sprintf("%s is a binary file (%d bytes), not text.", nt.Name, nt.Size)
kind, known := s.slotForExtension(ctx, a.Principal, ext)
if known {
if list, _, err := s.Skills(ctx, a.Principal, a.ChatID); err == nil {
for _, sk := range list {
name := strings.ToLower(sk.Name)
for _, w := range kind.Words {
if strings.Contains(name, w) {
return msg + fmt.Sprintf(" Use the %s skill for %s files (load_skill).", sk.Name, ext)
}
}
}
}
}
switch {
case canRun && known && kind.Library != "":
return msg + fmt.Sprintf(" Run a script with the commands tool instead, for example with %s.", kind.Library)
case canRun:
return msg + " Run a script with the commands tool instead."
}
return msg + " This chat has no tool that opens it."
}
// slotForExtension finds the slot that covers files with the extension ext.
// The slots come from the skills the add-ons declare, then from the default
// slots (data in the contract); the host holds no list of formats of its own.
func (s *Service) slotForExtension(ctx context.Context, p, ext string) (contract.Slot, bool) {
var sets []AddonSkillSet
if lister, ok := s.d.AddonTools.(AddonSkillLister); ok && s.d.AddonTools != nil {
sets, _ = lister.AddonSkills(ctx, p)
}
for _, set := range sets {
for _, sl := range set.Slots {
if sl.Kind == contract.SlotKindDocuments && slices.Contains(sl.Extensions, ext) {
return sl, true
}
}
}
for _, sl := range contract.DefaultSlots() {
if sl.Kind == contract.SlotKindDocuments && slices.Contains(sl.Extensions, ext) {
return sl, true
}
}
return contract.Slot{}, false
}