Files
LocalAI/core/services/studio/connectorplace.go
T
Ettore Di Giacinto ab78cd0301 feat(studio): propose triggers from a connector chat and report back there
A connector chat can now propose a trigger, the person answers it in the
chat, and the run reports to the chat.

The trigger tools propose_trigger and list_triggers are offered in a
connector chat when a capability gate allows triggers.propose for the
binding. The gate is a small interface on the service with a deny-all
default, so nothing changes until the application sets one. Change and
delete stay in Studio.

A proposal from a chat stores the binding, chat and thread, a four
character code and a 24 hour expiry. The host writes the proposal and the
code in that chat. "confirm CODE" or "cancel CODE", as a whole message in
the same chat and thread, is handled before any model turn and needs
proposals.confirm. A binding may make 3 proposals an hour. Proposals that
lapse are marked expired, and the proposal card shows that state.

A trigger carries a delivery: the Inbox (default) or the chat of the
proposal, with the modes always, on_change and on_problem. The host writes
the last message of the run, cut and with broadcast mentions broken; a
message of exactly [SILENT] writes nothing; three failures in a row raise
one alert per 6 hours. A failed delivery falls back to the Inbox. Host
messages of a binding go through an outbox of 20 a minute that queues the
rest. While triggers.run is off, runs of such a trigger are skipped, not
replayed, and the record is unchanged. The audit records each step with a
hash and no text.

Assisted-by: Claude:claude-sonnet-5-5 go test ginkgo eslint
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 14:00:31 +00:00

59 lines
2.2 KiB
Go

package studio
import (
"context"
"strings"
"github.com/mudler/LocalAI/core/services/studio/connectors"
"github.com/mudler/LocalAI/pkg/addons/contract"
)
// The place of a connector chat is where the platform shows it: the chat and
// the thread of the last message the bot answered. A tool that runs in the
// turn, and the host when a run reports back, need it to write to the same
// place. A connector chat is made for a sender or a thread, not for one
// platform chat, so the host keeps the place of each chat next to the chat
// mapping, under a key that cannot be the ID of a sender. Nothing in the
// store changes: the chat column holds "chat\x1fthread".
const (
placeKeyPrefix = "place:"
placeSep = "\x1f"
// maxPlaceValue is the width of the chat column of the stores.
maxPlaceValue = 128
)
func placeKey(chatID string) string { return placeKeyPrefix + chatID }
func encodePlace(chat, thread string) string { return chat + placeSep + thread }
func decodePlace(v string) (chat, thread string, ok bool) {
chat, thread, ok = strings.Cut(v, placeSep)
return chat, thread, ok && chat != ""
}
// rememberPlace keeps where the message m was written, for the chat chatID of
// binding b. A place that does not fit the store is not kept: the chat then
// has no place, and what needs one falls back to the Inbox.
func (c *Connectors) rememberPlace(ctx context.Context, b connectors.Binding, chatID string, m contract.ConnectorMessage) {
key, val := placeKey(chatID), encodePlace(m.Chat, m.Thread)
if len(key) > contract.MaxConnectorSender || len(val) > maxPlaceValue {
return
}
if cur, ok, err := c.o.Store.ChatOf(ctx, b.ID, key); err == nil && ok && cur == val {
return
}
if err := c.o.Store.SetChat(ctx, b.ID, key, val); err != nil {
c.log.Warn("studio: could not keep the place of a connector chat", "binding", b.ID, "error", err)
}
}
// PlaceOf returns the platform chat and thread where the connector chat chatID
// of the binding was last written to. It is false when the chat has none.
func (c *Connectors) PlaceOf(ctx context.Context, bindingID, chatID string) (chat, thread string, ok bool) {
v, found, err := c.o.Store.ChatOf(ctx, bindingID, placeKey(chatID))
if err != nil || !found {
return "", "", false
}
return decodePlace(v)
}