mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 20:43:27 -05:00
A chat now enables a set of connections for an add-on whose tools run with
a credential, and the model names one of them in each call. This applies to
every add-on that declares a credential in its manifest.
- The Tools menu row of the add-on lists the connections with a checkbox
each. The saved agent names a set too (an empty set means each chat
chooses). A chat that a connector binding started keeps that binding
alone. With one connection, it is the set. With none enabled, the tools
are not offered.
- With one connection enabled the tools are unchanged. With two or more,
the host adds a required `connection` argument to every tool of the
add-on in the definitions it gives the model: an enum of the connection
names (the display name folded to lower case, digits and hyphens; the
description keeps the display name). The add-on never sees the argument.
- The name is checked against the set the chat has now, not against all the
connections of the person. A missing or unknown name returns a sentence
that lists the allowed names, and nothing is sent. A write tool never runs
without the name. Text in a tool result cannot widen the set, and the
owner checks the chosen binding against the set again.
- The resource list of the named connection is the only one that applies.
Results, the approval card and the text for a pending change name the
connection; the audit line has the binding ID.
- A connection name must be unique per person and add-on among the
credential-only connections, compared by name fold. Older duplicates get
a numeric suffix in the model's list.
- The stored chat, draft and agent values move from one binding ID per
add-on to a list of IDs. A single string is read as a set of one in the
stores and in every request, so older data and clients keep working.
PUT /api/chats/{id}/connections takes `connections`; the older
`connection` field still works.
Assisted-by: Claude:claude-sonnet-5-5 go gofmt ginkgo node eslint playwright swag
101 lines
3.4 KiB
Go
101 lines
3.4 KiB
Go
package studio
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
|
|
"github.com/mudler/LocalAI/core/services/studio/connsets"
|
|
)
|
|
|
|
// ErrBadConnection means a choice names a connection that is not one of the
|
|
// person's own for that add-on, or that cannot be changed from here.
|
|
var ErrBadConnection = errors.New("studio: the connection is not one of yours for this add-on")
|
|
|
|
// ErrConnectionByAgent means the saved agent of the chat names the connections
|
|
// for this add-on, so the chat cannot choose others.
|
|
var ErrConnectionByAgent = fmt.Errorf("%w: the agent of this chat sets the connection", ErrBadConnection)
|
|
|
|
// maxConnectionSwitches bounds the add-ons for which one chat or one agent
|
|
// names connections.
|
|
const maxConnectionSwitches = 100
|
|
|
|
// maxConnectionsPerAddon bounds the connections enabled for one add-on.
|
|
const maxConnectionsPerAddon = 50
|
|
|
|
// checkConnections makes sure each entry (add-on ID to binding IDs) names
|
|
// bindings that the user owns for that add-on and that hold a token. The
|
|
// choice grants nothing by itself: the host still holds every call to the
|
|
// resource list of the binding.
|
|
func (s *Service) checkConnections(ctx context.Context, user string, m connsets.Sets) error {
|
|
if len(m) > maxConnectionSwitches {
|
|
return fmt.Errorf("%w: at most %d add-ons", ErrBadConnection, maxConnectionSwitches)
|
|
}
|
|
for addon, ids := range m {
|
|
if !addonIDShape.MatchString(addon) {
|
|
return fmt.Errorf("%w: %q", ErrBadAddonID, addon)
|
|
}
|
|
if len(ids) > maxConnectionsPerAddon {
|
|
return fmt.Errorf("%w: at most %d connections for one add-on", ErrBadConnection, maxConnectionsPerAddon)
|
|
}
|
|
if len(ids) == 0 {
|
|
continue
|
|
}
|
|
if s.connectors == nil {
|
|
return fmt.Errorf("%w: connections are off on this server", ErrBadConnection)
|
|
}
|
|
list, err := s.connectors.Connections(ctx, user, addon)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, id := range ids {
|
|
if !slices.ContainsFunc(list, func(c Connection) bool { return c.ID == id }) {
|
|
return ErrBadConnection
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetConnections sets the connections the chat may use for the tools of an
|
|
// add-on that runs with a credential. An empty list clears the choice.
|
|
// Choosing connections switches the add-on on for the chat. A chat whose agent
|
|
// names a set for the add-on cannot change it: ErrConnectionByAgent. The chat
|
|
// of another user is session.ErrNotFound.
|
|
func (s *Service) SetConnections(ctx context.Context, user, conv, addonID string, bindingIDs []string) error {
|
|
if !addonIDShape.MatchString(addonID) {
|
|
return fmt.Errorf("%w: %q", ErrBadAddonID, addonID)
|
|
}
|
|
rec, err := s.d.Store.GetChat(ctx, principal(user), conv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rec.AgentID != "" {
|
|
if ag, err := s.Agent(ctx, user, rec.AgentID); err == nil && len(ag.Connections[addonID]) > 0 {
|
|
return ErrConnectionByAgent
|
|
}
|
|
}
|
|
ids := connsets.Dedup(bindingIDs)
|
|
next := connsets.Clone(rec.Connections)
|
|
if len(ids) == 0 {
|
|
delete(next, addonID)
|
|
} else {
|
|
if err := s.checkConnections(ctx, user, connsets.Sets{addonID: ids}); err != nil {
|
|
return err
|
|
}
|
|
if next == nil {
|
|
next = connsets.Sets{}
|
|
}
|
|
next[addonID] = ids
|
|
}
|
|
if err := s.d.Store.SetConnections(ctx, principal(user), conv, next); err != nil {
|
|
return err
|
|
}
|
|
if len(ids) > 0 && slices.Contains(rec.AddonsOff, addonID) {
|
|
off := slices.DeleteFunc(slices.Clone(rec.AddonsOff), func(id string) bool { return id == addonID })
|
|
return s.d.Store.SetAddonsOff(ctx, principal(user), conv, off)
|
|
}
|
|
return nil
|
|
}
|