mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
feat(addons): add the history op to the connector contract
A connector kind can declare history.thread in its manifest (api 2) and then serves the op history: the earlier messages of one thread, oldest first, bounded by a limit. A kind that declares nothing is never asked. Assisted-by: Claude Code:claude-sonnet-5-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
3 files changed
+119
-4
No files matched your search
@@ -23,8 +23,9 @@ import (
|
||||
// the add-on calls the host operation ConnectorInboundOp.
|
||||
// - Outbound. The host calls "/connector" with the ops "send", "edit",
|
||||
// "typing", "send_file" and "me", "media" to read a file a message
|
||||
// carries, and "draft" for a kind that declares drafts. The host passes
|
||||
// the token on every call; the add-on never stores it.
|
||||
// carries, "draft" for a kind that declares drafts and "history" for a
|
||||
// kind that declares history. The host passes the token on every call;
|
||||
// the add-on never stores it.
|
||||
|
||||
// Delivery says how messages reach the host.
|
||||
const (
|
||||
@@ -55,6 +56,10 @@ const (
|
||||
// that is not a message yet. The platform replaces it when the answer
|
||||
// is sent. Only a kind with Drafts serves it.
|
||||
ConnectorOpDraft = "draft"
|
||||
// ConnectorOpHistory returns the earlier messages of the thread
|
||||
// ConnectorRequest.Thread of Chat, oldest first. Only a kind with
|
||||
// History serves it; the host never calls it for another kind.
|
||||
ConnectorOpHistory = "history"
|
||||
)
|
||||
|
||||
// Codes of a ConnectorError.
|
||||
@@ -89,6 +94,17 @@ const (
|
||||
// MinPreviewMs and MaxPreviewMs bound ConnectorKind.PreviewMs.
|
||||
MinPreviewMs = 250
|
||||
MaxPreviewMs = 10000
|
||||
// MinHistoryAPI is the first manifest API version that knows the History
|
||||
// ability of a connector kind.
|
||||
MinHistoryAPI uint64 = 2
|
||||
// DefaultHistoryMessages is how many messages the host asks the op
|
||||
// history for when the kind declares no limit of its own.
|
||||
DefaultHistoryMessages = 30
|
||||
// MaxHistoryMessages bounds ConnectorHistory.MaxMessages and the Limit of
|
||||
// a history request.
|
||||
MaxHistoryMessages = 200
|
||||
// MaxHistoryMedia is how many file names one history message carries.
|
||||
MaxHistoryMedia = 8
|
||||
)
|
||||
|
||||
// ConnectorKind is one kind of binding an add-on serves, such as telegram.
|
||||
@@ -114,6 +130,35 @@ type ConnectorKind struct {
|
||||
// of an answer (an edit or a draft) of one binding. Zero: the host's
|
||||
// default, one second. Else MinPreviewMs to MaxPreviewMs.
|
||||
PreviewMs int `json:"previewMs,omitempty"`
|
||||
// History declares that the add-on serves the op history. A kind without
|
||||
// it keeps the behaviour of a platform that cannot be asked for old
|
||||
// messages. It needs manifest api MinHistoryAPI.
|
||||
History *ConnectorHistory `json:"history,omitempty"`
|
||||
}
|
||||
|
||||
// ConnectorHistory is the ability to read old messages of a platform.
|
||||
type ConnectorHistory struct {
|
||||
// Thread says that the op history returns the earlier messages of a
|
||||
// thread. It must be true: it is the only ability so far.
|
||||
Thread bool `json:"thread"`
|
||||
// MaxMessages is the most messages one call returns. Zero: the host
|
||||
// asks for DefaultHistoryMessages. At most MaxHistoryMessages.
|
||||
MaxMessages int `json:"maxMessages,omitempty"`
|
||||
}
|
||||
|
||||
// ServesThreadHistory reports whether the kind answers the op history for threads.
|
||||
func (k ConnectorKind) ServesThreadHistory() bool { return k.History != nil && k.History.Thread }
|
||||
|
||||
// HistoryLimit is how many messages the host asks for at most: the limit it
|
||||
// wants (zero: DefaultHistoryMessages), cut to what the kind declares.
|
||||
func (k ConnectorKind) HistoryLimit(want int) int {
|
||||
if want <= 0 || want > MaxHistoryMessages {
|
||||
want = DefaultHistoryMessages
|
||||
}
|
||||
if k.History != nil && k.History.MaxMessages > 0 && k.History.MaxMessages < want {
|
||||
want = k.History.MaxMessages
|
||||
}
|
||||
return want
|
||||
}
|
||||
|
||||
// ConnectorCapability is the manifest field capabilities.connector.
|
||||
@@ -121,7 +166,7 @@ type ConnectorCapability struct {
|
||||
Kinds []ConnectorKind `json:"kinds"`
|
||||
}
|
||||
|
||||
func validateConnector(c *ConnectorCapability, ui *UI) error {
|
||||
func validateConnector(c *ConnectorCapability, ui *UI, api VersionRange) error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -143,6 +188,14 @@ func validateConnector(c *ConnectorCapability, ui *UI) error {
|
||||
if k.PreviewMs != 0 && (k.PreviewMs < MinPreviewMs || k.PreviewMs > MaxPreviewMs) {
|
||||
return fmt.Errorf("connector previewMs must be 0 or %d to %d", MinPreviewMs, MaxPreviewMs)
|
||||
}
|
||||
if h := k.History; h != nil {
|
||||
if api.Max < MinHistoryAPI {
|
||||
return fmt.Errorf("connector history needs api %d", MinHistoryAPI)
|
||||
}
|
||||
if !h.Thread || h.MaxMessages < 0 || h.MaxMessages > MaxHistoryMessages {
|
||||
return fmt.Errorf("connector history needs thread and maxMessages 0 to %d", MaxHistoryMessages)
|
||||
}
|
||||
}
|
||||
if k.Page != "" {
|
||||
ok := false
|
||||
if ui != nil {
|
||||
@@ -232,6 +285,12 @@ type ConnectorRequest struct {
|
||||
DraftID int64 `json:"draftId,omitempty"`
|
||||
// media: one reference of ConnectorMessage.Media.
|
||||
Media string `json:"media,omitempty"`
|
||||
// history: Chat and Thread name the thread. Limit is the most messages to
|
||||
// return (1 to MaxHistoryMessages): the newest ones before Before. Before
|
||||
// is the ID of the message that the host handles now; it and the later
|
||||
// messages are not returned.
|
||||
Limit int `json:"limit,omitempty"`
|
||||
Before string `json:"before,omitempty"`
|
||||
// send_file: the file. Its name is SafeFileName of itself and its data
|
||||
// is 1 to MaxConnectorFileBytes long.
|
||||
File *ConnectorFile `json:"file,omitempty"`
|
||||
@@ -268,6 +327,29 @@ type ConnectorResponse struct {
|
||||
BotUsername string `json:"botUsername,omitempty"`
|
||||
// media
|
||||
File *ConnectorFile `json:"file,omitempty"`
|
||||
// history: the messages, oldest first, at most the Limit asked for.
|
||||
// Truncated says that older messages exist that are not returned.
|
||||
Messages []ConnectorHistoryMessage `json:"messages,omitempty"`
|
||||
Truncated bool `json:"truncated,omitempty"`
|
||||
}
|
||||
|
||||
// ConnectorHistoryMessage is one earlier message of a thread. The text is
|
||||
// what other people wrote: the host treats it as untrusted content.
|
||||
type ConnectorHistoryMessage struct {
|
||||
MessageID string `json:"messageId,omitempty"`
|
||||
SenderID string `json:"senderId,omitempty"`
|
||||
SenderName string `json:"senderName,omitempty"`
|
||||
SenderUsername string `json:"senderUsername,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
// TS is the time of the message, when the platform tells it.
|
||||
TS time.Time `json:"ts,omitzero"`
|
||||
// Own is true for a message of the bot itself.
|
||||
Own bool `json:"own,omitempty"`
|
||||
// Media is how many files the message carries; MediaNames names them
|
||||
// when the platform does, at most MaxHistoryMedia. The bytes are never
|
||||
// part of a history.
|
||||
Media int `json:"media,omitempty"`
|
||||
MediaNames []string `json:"mediaNames,omitempty"`
|
||||
}
|
||||
|
||||
// ConnectorRuntime is a runtime that serves the connector route. An add-on
|
||||
|
||||
@@ -42,6 +42,39 @@ var _ = Describe("the connector capability", func() {
|
||||
m.Capabilities.Connector.Kinds[0].Page = ""
|
||||
Expect(ValidateManifest(m, host())).To(Succeed())
|
||||
})
|
||||
Describe("the history ability", func() {
|
||||
kind := func() *ConnectorKind { return &m.Capabilities.Connector.Kinds[0] }
|
||||
BeforeEach(func() { m.API = VersionRange{Min: 1, Max: 2} })
|
||||
|
||||
It("accepts a thread history with the manifest api 2", func() {
|
||||
kind().History = &ConnectorHistory{Thread: true, MaxMessages: 50}
|
||||
Expect(ValidateManifest(m, host())).To(Succeed())
|
||||
Expect(kind().ServesThreadHistory()).To(BeTrue())
|
||||
})
|
||||
It("refuses it under the manifest api 1", func() {
|
||||
m.API = VersionRange{Min: 1, Max: 1}
|
||||
kind().History = &ConnectorHistory{Thread: true}
|
||||
Expect(ValidateManifest(m, host())).NotTo(Succeed())
|
||||
})
|
||||
It("refuses an ability that says nothing and a limit out of bounds", func() {
|
||||
kind().History = &ConnectorHistory{}
|
||||
Expect(ValidateManifest(m, host())).NotTo(Succeed())
|
||||
kind().History = &ConnectorHistory{Thread: true, MaxMessages: MaxHistoryMessages + 1}
|
||||
Expect(ValidateManifest(m, host())).NotTo(Succeed())
|
||||
kind().History = &ConnectorHistory{Thread: true, MaxMessages: -1}
|
||||
Expect(ValidateManifest(m, host())).NotTo(Succeed())
|
||||
})
|
||||
It("serves nothing for a kind that declares nothing", func() {
|
||||
Expect(kind().ServesThreadHistory()).To(BeFalse())
|
||||
})
|
||||
It("cuts the limit to what the kind declares", func() {
|
||||
k := ConnectorKind{History: &ConnectorHistory{Thread: true, MaxMessages: 10}}
|
||||
Expect(k.HistoryLimit(0)).To(Equal(10))
|
||||
Expect(k.HistoryLimit(5)).To(Equal(5))
|
||||
Expect(k.HistoryLimit(500)).To(Equal(10))
|
||||
Expect(ConnectorKind{History: &ConnectorHistory{Thread: true}}.HistoryLimit(0)).To(Equal(DefaultHistoryMessages))
|
||||
})
|
||||
})
|
||||
DescribeTable("refuses",
|
||||
func(edit func(*ConnectorCapability)) {
|
||||
edit(m.Capabilities.Connector)
|
||||
|
||||
@@ -403,7 +403,7 @@ func ValidateManifest(m Manifest, h HostCompatibility) error {
|
||||
if e := validateResourceList(c, m.API); e != nil {
|
||||
return e
|
||||
}
|
||||
if e := validateConnector(c.Connector, c.UI); e != nil {
|
||||
if e := validateConnector(c.Connector, c.UI, m.API); e != nil {
|
||||
return e
|
||||
}
|
||||
if e := validateSkillDefinitions(m.ID, c.SkillDefinitions); e != nil {
|
||||
|
||||
Reference in new issue
Block a user