mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
feat(addons): let a connector kind declare its file abilities
A connector kind can declare files.receive and files.send, each with a maximum size and optional media types. The field is additive and needs manifest api 2. A kind that declares nothing keeps the inline path. 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
+174
No files matched your search
@@ -141,6 +141,10 @@ type ConnectorKind struct {
|
||||
// this declaration and sends it with the op poll. It needs manifest api
|
||||
// MinHistoryAPI.
|
||||
Options []ConnectorOption `json:"options,omitempty"`
|
||||
// Files declares the files the kind can receive and send, with a limit
|
||||
// for each direction. Without it, the host moves files inline and caps
|
||||
// them at MaxConnectorFileBytes. It needs manifest api MinFilesAPI.
|
||||
Files *ConnectorFiles `json:"files,omitempty"`
|
||||
}
|
||||
|
||||
// ConnectorOption is one choice among a few values.
|
||||
@@ -285,6 +289,9 @@ func validateConnector(c *ConnectorCapability, ui *UI, api VersionRange) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := k.Files.validate(api); err != nil {
|
||||
return err
|
||||
}
|
||||
if k.Page != "" {
|
||||
ok := false
|
||||
if ui != nil {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
package contract
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// MinFilesAPI is the first manifest API version that knows the files ability
|
||||
// of a connector kind. A manifest that declares one must accept it.
|
||||
const MinFilesAPI uint64 = 2
|
||||
|
||||
// Limits of the files ability.
|
||||
const (
|
||||
// MaxDeclaredFileBytes is the largest limit a kind may declare: 1 TiB.
|
||||
// The host ceiling and the owner's settings still apply.
|
||||
MaxDeclaredFileBytes int64 = 1 << 40
|
||||
// MaxFileTypes bounds the media types of one direction.
|
||||
MaxFileTypes = 32
|
||||
)
|
||||
|
||||
// FileAbility is what a connector kind can move in one direction.
|
||||
type FileAbility struct {
|
||||
// MaxBytes is the largest file the platform lets the add-on move, in
|
||||
// bytes. The host uses it as an upper bound that the owner can lower. It
|
||||
// is not proof that a transfer works.
|
||||
MaxBytes int64 `json:"maxBytes"`
|
||||
// Types lists the media types the kind moves, such as application/pdf or
|
||||
// image/*. Empty means every type the host allows.
|
||||
Types []string `json:"types,omitempty"`
|
||||
}
|
||||
|
||||
// ConnectorFiles is the manifest field files of a connector kind. A kind that
|
||||
// declares none keeps the inline path, capped by MaxConnectorFileBytes.
|
||||
type ConnectorFiles struct {
|
||||
// Receive is the ability to read files that people send to the bot.
|
||||
Receive *FileAbility `json:"receive,omitempty"`
|
||||
// Send is the ability to upload files from the bot.
|
||||
Send *FileAbility `json:"send,omitempty"`
|
||||
}
|
||||
|
||||
var mediaTypePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9!#$&^_.+-]{0,63}/(\*|[a-z0-9][a-z0-9!#$&^_.+-]{0,63})$`)
|
||||
|
||||
func (a *FileAbility) validate(dir string) error {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
if a.MaxBytes < 1 || a.MaxBytes > MaxDeclaredFileBytes {
|
||||
return fmt.Errorf("files.%s.maxBytes must be 1 to %d", dir, MaxDeclaredFileBytes)
|
||||
}
|
||||
if len(a.Types) > MaxFileTypes {
|
||||
return fmt.Errorf("files.%s.types has more than %d entries", dir, MaxFileTypes)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, t := range a.Types {
|
||||
if !mediaTypePattern.MatchString(t) || seen[t] {
|
||||
return fmt.Errorf("files.%s.types needs distinct media types such as application/pdf or image/*", dir)
|
||||
}
|
||||
seen[t] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *ConnectorFiles) validate(api VersionRange) error {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
if api.Max < MinFilesAPI {
|
||||
return fmt.Errorf("a connector kind that declares files needs api %d", MinFilesAPI)
|
||||
}
|
||||
if f.Receive == nil && f.Send == nil {
|
||||
return fmt.Errorf("files must declare receive, send or both")
|
||||
}
|
||||
if err := f.Receive.validate("receive"); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Send.validate("send")
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
package contract
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("the files ability of a connector kind", func() {
|
||||
var m Manifest
|
||||
BeforeEach(func() {
|
||||
b, err := os.ReadFile("../../../tests/addons/fixtures/contracts/page.json")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
m, err = ParseManifest(b)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
m.API = VersionRange{Min: 1, Max: 2}
|
||||
m.Capabilities.UI = nil
|
||||
m.Capabilities.Connector = &ConnectorCapability{Kinds: []ConnectorKind{{ID: "telegram", Title: "Telegram", Delivery: DeliveryPull}}}
|
||||
})
|
||||
host := func() HostCompatibility {
|
||||
h := compatible()
|
||||
h.API, h.APIMin = HostAPIVersion, HostAPIMin
|
||||
h.SDK = HostSDKVersion
|
||||
return h
|
||||
}
|
||||
declare := func(f *ConnectorFiles) { m.Capabilities.Connector.Kinds[0].Files = f }
|
||||
|
||||
It("accepts a kind that declares what it receives and sends", func() {
|
||||
declare(&ConnectorFiles{
|
||||
Receive: &FileAbility{MaxBytes: 20 << 20},
|
||||
Send: &FileAbility{MaxBytes: 50 << 20, Types: []string{"application/pdf", "image/*"}},
|
||||
})
|
||||
Expect(ValidateManifest(m, host())).To(Succeed())
|
||||
})
|
||||
|
||||
It("accepts a kind that declares one direction only, or nothing", func() {
|
||||
declare(&ConnectorFiles{Send: &FileAbility{MaxBytes: 1 << 20}})
|
||||
Expect(ValidateManifest(m, host())).To(Succeed())
|
||||
declare(nil)
|
||||
Expect(ValidateManifest(m, host())).To(Succeed())
|
||||
})
|
||||
|
||||
It("needs a manifest that accepts API 2", func() {
|
||||
declare(&ConnectorFiles{Receive: &FileAbility{MaxBytes: 1 << 20}})
|
||||
m.API = VersionRange{Min: 1, Max: 1}
|
||||
Expect(ValidateManifest(m, host())).To(MatchError(ContainSubstring("api 2")))
|
||||
})
|
||||
|
||||
DescribeTable("refuses",
|
||||
func(f *ConnectorFiles) {
|
||||
declare(f)
|
||||
Expect(ValidateManifest(m, host())).NotTo(Succeed())
|
||||
},
|
||||
Entry("a limit of zero", &ConnectorFiles{Receive: &FileAbility{}}),
|
||||
Entry("a negative limit", &ConnectorFiles{Send: &FileAbility{MaxBytes: -1}}),
|
||||
Entry("a limit above the largest the host knows", &ConnectorFiles{Send: &FileAbility{MaxBytes: MaxDeclaredFileBytes + 1}}),
|
||||
Entry("an empty ability", &ConnectorFiles{}),
|
||||
Entry("a type that is no media type", &ConnectorFiles{Send: &FileAbility{MaxBytes: 1, Types: []string{"pdf"}}}),
|
||||
Entry("a repeated type", &ConnectorFiles{Send: &FileAbility{MaxBytes: 1, Types: []string{"image/*", "image/*"}}}),
|
||||
Entry("too many types", &ConnectorFiles{Send: &FileAbility{MaxBytes: 1, Types: manyTypes(MaxFileTypes + 1)}}),
|
||||
)
|
||||
|
||||
It("reads files from JSON and refuses an unknown field in it", func() {
|
||||
var k ConnectorKind
|
||||
Expect(json.Unmarshal([]byte(`{"id":"x","title":"X","delivery":"pull","files":{"receive":{"maxBytes":10,"types":["text/plain"]}}}`), &k)).To(Succeed())
|
||||
Expect(k.Files.Receive.MaxBytes).To(Equal(int64(10)))
|
||||
Expect(k.Files.Send).To(BeNil())
|
||||
declare(&ConnectorFiles{Receive: &FileAbility{MaxBytes: 1}})
|
||||
raw, err := json.Marshal(m)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
_, err = ParseManifest(raw)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
bad := strings.Replace(string(raw), `"maxBytes":1`, `"maxBytes":1,"bogus":2`, 1)
|
||||
_, err = ParseManifest([]byte(bad))
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
func manyTypes(n int) []string {
|
||||
out := make([]string, n)
|
||||
for i := range out {
|
||||
out[i] = "text/x-" + strings.Repeat("a", i+1)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user