mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
feat(studio): add the thread_context switch to connector bindings
A binding reads the earlier messages of a thread by default. The new thread_context option turns that off, and the kind list says which kinds can give a thread's messages. It is stored like send_files: the zero value keeps the default. Assisted-by: Claude Code:claude-sonnet-5-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
10 files changed
+114
-9
No files matched your search
@@ -24,7 +24,7 @@ func connectorOut(i studiosvc.ConnectorInfo) schema.ChatsConnector {
|
||||
}
|
||||
return schema.ChatsConnector{ID: i.ID, Name: i.Name, Kind: i.Kind, Addon: i.AddonID, Active: i.Active, Reason: i.Reason, AgentID: i.AgentID, Enabled: i.Enabled, AcceptAny: i.AcceptAny,
|
||||
Allow: allow, NotifyChat: i.NotifyChat, HasToken: i.HasToken, Created: i.Created,
|
||||
ReplyMode: i.ReplyMode(), Conversation: i.ConversationMode(), WindowMinutes: i.WindowMinutes, SendFiles: i.SendsFiles()}
|
||||
ReplyMode: i.ReplyMode(), Conversation: i.ConversationMode(), WindowMinutes: i.WindowMinutes, SendFiles: i.SendsFiles(), ThreadContext: i.ReadsThread()}
|
||||
}
|
||||
|
||||
// acceptAnyConfirmation is what a request must say to turn on accept_any.
|
||||
@@ -38,7 +38,7 @@ func failAcceptAny(c echo.Context) error {
|
||||
func connectorIn(req schema.ChatsConnectorWrite) studiosvc.ConnectorInput {
|
||||
return studiosvc.ConnectorInput{Kind: req.Kind, Name: req.Name, Token: req.Token, AgentID: req.AgentID, Enabled: req.Enabled,
|
||||
AcceptAny: req.AcceptAny, Allow: req.Allow, NotifyChat: req.NotifyChat,
|
||||
Reply: req.ReplyMode, Conversation: req.Conversation, WindowMinutes: req.WindowMinutes, SendFiles: req.SendFiles}
|
||||
Reply: req.ReplyMode, Conversation: req.Conversation, WindowMinutes: req.WindowMinutes, SendFiles: req.SendFiles, ThreadContext: req.ThreadContext}
|
||||
}
|
||||
|
||||
var connectorRules = []errRule{
|
||||
@@ -99,14 +99,14 @@ func (h *Handlers) ConnectorKinds(c echo.Context) error {
|
||||
}
|
||||
out := schema.ChatsConnectorKindList{Kinds: make([]schema.ChatsConnectorKind, 0, len(kinds))}
|
||||
for _, k := range kinds {
|
||||
out.Kinds = append(out.Kinds, schema.ChatsConnectorKind{Addon: k.AddonID, Kind: k.ID, Title: k.Title, Description: k.Description, SenderHint: k.SenderHint, Page: k.Page})
|
||||
out.Kinds = append(out.Kinds, schema.ChatsConnectorKind{Addon: k.AddonID, Kind: k.ID, Title: k.Title, Description: k.Description, SenderHint: k.SenderHint, Page: k.Page, ThreadHistory: k.ServesThreadHistory()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, out)
|
||||
}
|
||||
|
||||
// CreateConnector creates a binding.
|
||||
// @Summary Create a connector binding.
|
||||
// @Description Needs the kind (the platform), the bot token (kept sealed, never returned) and an agent of yours that names a model. The token is checked with the platform when its add-on runs: a token that the platform refuses is a 400. reply_mode (off, progress, stream; default progress), conversation (sender, chat, window with window_minutes, thread; default sender) and send_files (default true) say how the bot answers. The allowlist is empty at first, so nobody is accepted: add chat IDs, user IDs or @usernames, or set accept_any (anyone who can message the bot then uses the agent's tools; the request that turns it on must also carry confirm_accept_any set to "accept any message", or it is a 400). At most 10 bindings.
|
||||
// @Description Needs the kind (the platform), the bot token (kept sealed, never returned) and an agent of yours that names a model. The token is checked with the platform when its add-on runs: a token that the platform refuses is a 400. reply_mode (off, progress, stream; default progress), conversation (sender, chat, window with window_minutes, thread; default sender) and send_files (default true) say how the bot answers. thread_context (default true) lets the bot read the earlier messages of the thread in which it is mentioned, on a platform that can give them. The allowlist is empty at first, so nobody is accepted: add chat IDs, user IDs or @usernames, or set accept_any (anyone who can message the bot then uses the agent's tools; the request that turns it on must also carry confirm_accept_any set to "accept any message", or it is a 400). At most 10 bindings.
|
||||
// @Tags chats
|
||||
// @Param request body schema.ChatsConnectorWrite true "the binding"
|
||||
// @Success 201 {object} schema.ChatsConnector "The binding"
|
||||
|
||||
@@ -163,6 +163,19 @@ var _ = Describe("Chats connectors endpoints", func() {
|
||||
Expect(s.do("alice", http.MethodPost, "/api/chats/connectors/"+id+"/senders/99/allow", nil).Code).To(Equal(http.StatusBadRequest))
|
||||
})
|
||||
|
||||
It("stores the thread context switch: on for a new binding, kept by an update that omits it", func() {
|
||||
ag := agent("alice", "Context", "test-model")
|
||||
rec := create("alice", map[string]any{"name": "Context bot", "token": specToken, "agent_id": ag})
|
||||
Expect(rec.Code).To(Equal(http.StatusCreated), rec.Body.String())
|
||||
Expect(decodeJSON(rec)).To(HaveKeyWithValue("thread_context", true), "the default")
|
||||
id := decodeJSON(rec)["id"].(string)
|
||||
rec = s.do("alice", http.MethodPut, "/api/chats/connectors/"+id, map[string]any{"name": "Context bot", "agent_id": ag, "thread_context": false})
|
||||
Expect(rec.Code).To(Equal(http.StatusOK), rec.Body.String())
|
||||
Expect(decodeJSON(rec)).To(HaveKeyWithValue("thread_context", false))
|
||||
rec = s.do("alice", http.MethodPut, "/api/chats/connectors/"+id, map[string]any{"name": "Context bot", "agent_id": ag})
|
||||
Expect(decodeJSON(rec)).To(HaveKeyWithValue("thread_context", false), "an update without the field keeps it")
|
||||
})
|
||||
|
||||
It("stores the reply mode, the conversation mode and the file switch, and refuses unknown ones", func() {
|
||||
ag := agent("alice", "Modes", "test-model")
|
||||
rec := create("alice", map[string]any{"name": "Modes bot", "token": specToken, "agent_id": ag})
|
||||
|
||||
@@ -1177,6 +1177,10 @@ type ChatsConnector struct {
|
||||
// SendFiles sends the files that a turn makes to the sender, after the
|
||||
// answer.
|
||||
SendFiles bool `json:"send_files"`
|
||||
// ThreadContext lets the bot read the earlier messages of the thread in
|
||||
// which it is mentioned. It has an effect only on a platform that can
|
||||
// give them.
|
||||
ThreadContext bool `json:"thread_context"`
|
||||
// Active is false when the add-on that serves the binding is not running. The
|
||||
// binding is kept and works once the add-on is installed.
|
||||
Active bool `json:"active"`
|
||||
@@ -1193,6 +1197,9 @@ type ChatsConnectorKind struct {
|
||||
SenderHint string `json:"sender_hint,omitempty"`
|
||||
// Page is the add-on page that adds and edits bindings of this kind.
|
||||
Page string `json:"page,omitempty"`
|
||||
// ThreadHistory is true when the platform can give the earlier messages
|
||||
// of a thread, so a binding of this kind has the thread_context option.
|
||||
ThreadHistory bool `json:"thread_history,omitempty"`
|
||||
}
|
||||
|
||||
// ChatsConnectorKindList lists the connector kinds of the add-ons that run now.
|
||||
@@ -1227,6 +1234,9 @@ type ChatsConnectorWrite struct {
|
||||
// SendFiles turns the sending of a turn's files on or off. Absent means
|
||||
// on for a new binding and unchanged for an update.
|
||||
SendFiles *bool `json:"send_files,omitempty"`
|
||||
// ThreadContext turns the reading of the earlier messages of a thread on
|
||||
// or off. Absent means on for a new binding and unchanged for an update.
|
||||
ThreadContext *bool `json:"thread_context,omitempty"`
|
||||
}
|
||||
|
||||
// ChatsConnectorList is the response of the connector list endpoint.
|
||||
|
||||
@@ -70,6 +70,16 @@ func Run(newStore NewStore) {
|
||||
got, _ = s.GetBinding(ctx, "modes")
|
||||
Expect(got.ReplyMode()).To(Equal(connectors.ReplyProgress))
|
||||
Expect(got.SendsFiles()).To(BeTrue())
|
||||
Expect(got.ReadsThread()).To(BeTrue(), "a binding that says nothing reads the thread")
|
||||
})
|
||||
It("keeps the thread context switch", func() {
|
||||
s := newStore()
|
||||
b := binding("ctx", "u1", "Context")
|
||||
b.ThreadContextOff = true
|
||||
Expect(s.PutBinding(ctx, b)).To(Succeed())
|
||||
got, err := s.GetBinding(ctx, "ctx")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(got.ReadsThread()).To(BeFalse())
|
||||
})
|
||||
It("gives an old binding the add-on of its kind", func() {
|
||||
// A binding stored before connectors became add-ons has no add-on ID.
|
||||
|
||||
@@ -28,9 +28,12 @@ type bindingRow struct {
|
||||
Conversation string `gorm:"size:16"`
|
||||
WindowMinutes int
|
||||
FilesOff bool
|
||||
Token []byte
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
// ThreadContextOff is added by AutoMigrate; a row stored before it reads
|
||||
// the thread.
|
||||
ThreadContextOff bool
|
||||
Token []byte
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
}
|
||||
|
||||
func (bindingRow) TableName() string { return "studio_connectors" }
|
||||
@@ -84,7 +87,7 @@ func NewGORM(db *gorm.DB) (Store, error) {
|
||||
|
||||
func (r bindingRow) binding() (Binding, error) {
|
||||
b := Binding{ID: r.ID, Owner: r.Owner, Kind: r.Kind, AddonID: r.AddonID, Name: r.Name, AgentID: r.AgentID, Enabled: r.Enabled, AcceptAny: r.AcceptAny,
|
||||
NotifyChat: r.NotifyChat, Reply: r.Reply, Conversation: r.Conversation, WindowMinutes: r.WindowMinutes, FilesOff: r.FilesOff,
|
||||
NotifyChat: r.NotifyChat, Reply: r.Reply, Conversation: r.Conversation, WindowMinutes: r.WindowMinutes, FilesOff: r.FilesOff, ThreadContextOff: r.ThreadContextOff,
|
||||
Token: append([]byte(nil), r.Token...), Created: r.Created.UTC(), Updated: r.Updated.UTC()}
|
||||
if r.Allow != "" {
|
||||
if err := json.Unmarshal([]byte(r.Allow), &b.Allow); err != nil {
|
||||
@@ -102,7 +105,7 @@ func (s *gormStore) PutBinding(ctx context.Context, b Binding) error {
|
||||
}
|
||||
return s.db.WithContext(ctx).Save(&bindingRow{ID: b.ID, Owner: b.Owner, Kind: b.Kind, AddonID: b.AddonID, Name: b.Name, AgentID: b.AgentID, Enabled: b.Enabled,
|
||||
AcceptAny: b.AcceptAny, Allow: allow, NotifyChat: b.NotifyChat, Reply: b.Reply, Conversation: b.Conversation, WindowMinutes: b.WindowMinutes,
|
||||
FilesOff: b.FilesOff, Token: b.Token, Created: b.Created, Updated: b.Updated}).Error
|
||||
FilesOff: b.FilesOff, ThreadContextOff: b.ThreadContextOff, Token: b.Token, Created: b.Created, Updated: b.Updated}).Error
|
||||
}
|
||||
|
||||
func (s *gormStore) GetBinding(ctx context.Context, id string) (Binding, error) {
|
||||
|
||||
@@ -105,6 +105,10 @@ type Binding struct {
|
||||
// zero value sends them, so a binding stored before the option sends
|
||||
// them too.
|
||||
FilesOff bool
|
||||
// ThreadContextOff keeps the host from reading the earlier messages of
|
||||
// a thread when the bot is mentioned in it. The zero value reads them,
|
||||
// on a platform whose kind declares the ability.
|
||||
ThreadContextOff bool
|
||||
// Token is the bot token. In a Store it is sealed bytes; the Store returns
|
||||
// what it was given.
|
||||
Token []byte
|
||||
@@ -222,6 +226,10 @@ func (b Binding) ConversationMode() string {
|
||||
// SendsFiles tells whether the files a turn makes go to the sender.
|
||||
func (b Binding) SendsFiles() bool { return !b.FilesOff }
|
||||
|
||||
// ReadsThread tells whether the earlier messages of a thread are read when
|
||||
// the bot is mentioned in it.
|
||||
func (b Binding) ReadsThread() bool { return !b.ThreadContextOff }
|
||||
|
||||
// Allows tells whether a sender is on the allowlist. A match is by ID or, for an
|
||||
// @name entry, by username, ignoring case.
|
||||
func (b Binding) Allows(senderID, username string) bool {
|
||||
|
||||
@@ -37,6 +37,10 @@ type ConnectorInput struct {
|
||||
// SendFiles sends the files a turn makes to the sender. Nil means on for
|
||||
// a new binding and unchanged for an update.
|
||||
SendFiles *bool
|
||||
// ThreadContext lets the bot read the earlier messages of the thread in
|
||||
// which it is mentioned. Nil means on for a new binding and unchanged for
|
||||
// an update.
|
||||
ThreadContext *bool
|
||||
}
|
||||
|
||||
// apply sets the fields of the input that an owner writes on a binding.
|
||||
@@ -49,6 +53,9 @@ func (in ConnectorInput) apply(b *connectors.Binding) {
|
||||
if in.SendFiles != nil {
|
||||
b.FilesOff = !*in.SendFiles
|
||||
}
|
||||
if in.ThreadContext != nil {
|
||||
b.ThreadContextOff = !*in.ThreadContext
|
||||
}
|
||||
}
|
||||
|
||||
// ErrTokenRefused means the platform refused the bot token of a binding.
|
||||
|
||||
@@ -18329,6 +18329,10 @@ const docTemplate = `{
|
||||
"description": "SendFiles sends the files that a turn makes to the sender, after the\nanswer.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"thread_context": {
|
||||
"description": "ThreadContext lets the bot read the earlier messages of the thread in\nwhich it is mentioned. It has an effect only on a platform that can\ngive them.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"window_minutes": {
|
||||
"type": "integer"
|
||||
}
|
||||
@@ -19130,6 +19134,10 @@ const docTemplate = `{
|
||||
"description": "SendFiles sends the files that a turn makes to the sender, after the\nanswer.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"thread_context": {
|
||||
"description": "ThreadContext lets the bot read the earlier messages of the thread in\nwhich it is mentioned. It has an effect only on a platform that can\ngive them.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"window_minutes": {
|
||||
"type": "integer"
|
||||
}
|
||||
@@ -19154,6 +19162,10 @@ const docTemplate = `{
|
||||
"sender_hint": {
|
||||
"type": "string"
|
||||
},
|
||||
"thread_history": {
|
||||
"description": "ThreadHistory is true when the platform can give the earlier messages\nof a thread, so a binding of this kind has the thread_context option.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -19270,6 +19282,10 @@ const docTemplate = `{
|
||||
"description": "SendFiles turns the sending of a turn's files on or off. Absent means\non for a new binding and unchanged for an update.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"thread_context": {
|
||||
"description": "ThreadContext turns the reading of the earlier messages of a thread on\nor off. Absent means on for a new binding and unchanged for an update.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -18326,6 +18326,10 @@
|
||||
"description": "SendFiles sends the files that a turn makes to the sender, after the\nanswer.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"thread_context": {
|
||||
"description": "ThreadContext lets the bot read the earlier messages of the thread in\nwhich it is mentioned. It has an effect only on a platform that can\ngive them.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"window_minutes": {
|
||||
"type": "integer"
|
||||
}
|
||||
@@ -19127,6 +19131,10 @@
|
||||
"description": "SendFiles sends the files that a turn makes to the sender, after the\nanswer.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"thread_context": {
|
||||
"description": "ThreadContext lets the bot read the earlier messages of the thread in\nwhich it is mentioned. It has an effect only on a platform that can\ngive them.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"window_minutes": {
|
||||
"type": "integer"
|
||||
}
|
||||
@@ -19151,6 +19159,10 @@
|
||||
"sender_hint": {
|
||||
"type": "string"
|
||||
},
|
||||
"thread_history": {
|
||||
"description": "ThreadHistory is true when the platform can give the earlier messages\nof a thread, so a binding of this kind has the thread_context option.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -19267,6 +19279,10 @@
|
||||
"description": "SendFiles turns the sending of a turn's files on or off. Absent means\non for a new binding and unchanged for an update.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"thread_context": {
|
||||
"description": "ThreadContext turns the reading of the earlier messages of a thread on\nor off. Absent means on for a new binding and unchanged for an update.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -2857,6 +2857,12 @@ definitions:
|
||||
SendFiles sends the files that a turn makes to the sender, after the
|
||||
answer.
|
||||
type: boolean
|
||||
thread_context:
|
||||
description: |-
|
||||
ThreadContext lets the bot read the earlier messages of the thread in
|
||||
which it is mentioned. It has an effect only on a platform that can
|
||||
give them.
|
||||
type: boolean
|
||||
window_minutes:
|
||||
type: integer
|
||||
type: object
|
||||
@@ -3497,6 +3503,12 @@ definitions:
|
||||
SendFiles sends the files that a turn makes to the sender, after the
|
||||
answer.
|
||||
type: boolean
|
||||
thread_context:
|
||||
description: |-
|
||||
ThreadContext lets the bot read the earlier messages of the thread in
|
||||
which it is mentioned. It has an effect only on a platform that can
|
||||
give them.
|
||||
type: boolean
|
||||
window_minutes:
|
||||
type: integer
|
||||
type: object
|
||||
@@ -3514,6 +3526,11 @@ definitions:
|
||||
type: string
|
||||
sender_hint:
|
||||
type: string
|
||||
thread_history:
|
||||
description: |-
|
||||
ThreadHistory is true when the platform can give the earlier messages
|
||||
of a thread, so a binding of this kind has the thread_context option.
|
||||
type: boolean
|
||||
title:
|
||||
type: string
|
||||
type: object
|
||||
@@ -3599,6 +3616,11 @@ definitions:
|
||||
SendFiles turns the sending of a turn's files on or off. Absent means
|
||||
on for a new binding and unchanged for an update.
|
||||
type: boolean
|
||||
thread_context:
|
||||
description: |-
|
||||
ThreadContext turns the reading of the earlier messages of a thread on
|
||||
or off. Absent means on for a new binding and unchanged for an update.
|
||||
type: boolean
|
||||
token:
|
||||
type: string
|
||||
window_minutes:
|
||||
|
||||
Reference in new issue
Block a user