mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
test(studio): load_skill must load a document skill under an isolated Sandbox
The spec builds the turn tools under the product scope and then calls load_skill with a context of its own, as the harness does. It fails with the not_selected text on master. Assisted-by: Claude Code:claude-sonnet-5-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
2 files changed
+84
-1
No files matched your search
@@ -2,6 +2,7 @@
|
||||
package studio
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -24,10 +25,12 @@ type observationProvider struct {
|
||||
started chan struct{}
|
||||
release chan struct{}
|
||||
fail bool
|
||||
// addon is the add-on the observation is bound to; empty is renamed-provider.
|
||||
addon string
|
||||
}
|
||||
|
||||
func (p *observationProvider) NewExecutionObservation(ctx context.Context, user, conv string) (*executionrequest.Request, string, func() ExecutionDetail) {
|
||||
b := model.Binding{Actor: user, Addon: "renamed-provider", Generation: "1", Incarnation: "one", ExecutionTool: "run", StatusTool: "inspect"}
|
||||
b := model.Binding{Actor: user, Addon: cmp.Or(p.addon, "renamed-provider"), Generation: "1", Incarnation: "one", ExecutionTool: "run", StatusTool: "inspect"}
|
||||
r := executionrequest.New(ctx, b, "host-q", model.Eligible, executionrequest.IO{
|
||||
Validate: func(context.Context, model.Binding) error { return nil },
|
||||
Read: func(ctx context.Context, _ model.Binding, _ model.ObservationID, _ time.Time) (model.Payload, error) {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package studio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/mudler/LocalAI/core/services/studio/agents"
|
||||
"github.com/mudler/LocalAI/core/services/studio/session"
|
||||
model "github.com/mudler/LocalAI/pkg/addons/model/executionstatus"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
// observedBroker is a Sandbox whose isolation comes from the typed execution
|
||||
// status, read through the request scope as the real gateway reads it.
|
||||
type observedBroker struct {
|
||||
*documentBroker
|
||||
*observationProvider
|
||||
}
|
||||
|
||||
func (b *observedBroker) Tools(ctx context.Context, u string) ([]AddonTool, error) {
|
||||
return b.documentBroker.Tools(ctx, u)
|
||||
}
|
||||
func (b *observedBroker) Unavailable(ctx context.Context, u, a string, _ bool) string {
|
||||
if v, e := ExecutionObservation(ctx, u, a); v.Readiness == model.Unavailable {
|
||||
return e
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (b *observedBroker) Isolation(ctx context.Context, u, a string) string {
|
||||
v, _ := ExecutionObservation(ctx, u, a)
|
||||
if v.Readiness != model.Available || v.Isolation == model.UnknownIsolation {
|
||||
return ""
|
||||
}
|
||||
return string(v.Isolation)
|
||||
}
|
||||
|
||||
var _ = Describe("load_skill as the harness calls it", func() {
|
||||
It("loads a document skill when the tool runs outside the turn's context values", func() {
|
||||
ctx := context.Background()
|
||||
b := &observedBroker{
|
||||
documentBroker: &documentBroker{skillBroker: &skillBroker{}, defs: []AddonTool{{AddonID: "sandbox", Name: "addon__sandbox__exec", Shadows: "bash", Execution: sandboxExecution}}},
|
||||
observationProvider: &observationProvider{addon: "sandbox"},
|
||||
}
|
||||
svc, f := newSkillService(b.skillBroker)
|
||||
svc.d.AddonTools = b
|
||||
agentsStore, err := agents.NewJSON("")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
svc.d.Agents = agentsStore
|
||||
Expect(f.SyncAddonSkills(ctx, "sandbox", fstest.MapFS{"pptx/SKILL.md": {Data: skillMD("pptx", "Sandbox slides.", "Slides body.", "requires: [exec]")}})).To(Succeed())
|
||||
b.set(AddonSkillSet{AddonID: "sandbox", Skills: []string{"pptx"}, Shadows: []string{"pptx"}, Isolated: []string{"pptx"}, Running: true})
|
||||
agent, err := svc.SaveAgent(ctx, "alice", agents.Agent{Name: "Slides", Model: "m", Addons: []string{"sandbox"}, Skills: []string{"pptx"}})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
parent, err := svc.Create(ctx, "alice", CreateInput{Model: "m"})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
rec := session.ChatRecord{ID: "slides-child", Principal: "alice", Model: "m", Kind: session.KindSubAgent, ParentID: parent.ID, AgentID: agent.ID}
|
||||
Expect(svc.d.Store.CreateChat(ctx, rec)).To(Succeed())
|
||||
x, err := svc.ChatAccess(ctx, "alice", rec.ID)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// The turn builds its tools under the product scope, as the session does.
|
||||
turnCtx, closeScope := svc.productScope(ctx, x)
|
||||
defer closeScope()
|
||||
turn, err := svc.chatTools(turnCtx, x)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
var loader session.Tool
|
||||
for _, tool := range turn.Tools {
|
||||
if tool.Name == "load_skill" {
|
||||
loader = tool
|
||||
}
|
||||
}
|
||||
Expect(loader.Run).NotTo(BeNil(), "the tool is offered at the start of the turn")
|
||||
|
||||
// The harness calls the tool with a context of its own.
|
||||
body, err := loader.Run(context.Background(), json.RawMessage(`{"name":"pptx"}`))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(body).To(ContainSubstring("Slides body."))
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user