* WIP
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Drop old webui
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Almost there
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* It builds
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Make it build
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fixups, still doesn't work
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* unused now
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Send result before closing
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Fix observability
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Drop MCP code and wire-up in cogito
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Drop some templates
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Keep reporting into conv
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* tests fixups
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* tests fixups
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fixups
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Do not complete observable during thought process
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Update cogito
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Fixups
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Fixups
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Fixups
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Drop unneded option now
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Fixups
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Better handling of user tools
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* TEST
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Add flake attempts
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Revert "TEST"
This reverts commit 8b12a9fd03.
* tKeep indexing MCP actions
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Split CI jobs to improve speed
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* CI optimizations
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Bump timeout
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* Bump cogito
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fix: always commit last progress
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* chore: better management of observables
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
---------
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
353 lines
12 KiB
Go
353 lines
12 KiB
Go
package e2e_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
localagi "github.com/mudler/LocalAGI/pkg/client"
|
|
"github.com/mudler/LocalAGI/pkg/utils/ptr"
|
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
|
"github.com/sashabaranov/go-openai/jsonschema"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("E2E test", func() {
|
|
Context("Creates an agent and it answers", Label("E2E"), func() {
|
|
BeforeEach(func() {
|
|
Eventually(func() error {
|
|
// test apiURL is working and available
|
|
_, err := http.Get(localagiURL + "/readyz")
|
|
return err
|
|
}, "10m", "10s").ShouldNot(HaveOccurred())
|
|
|
|
client := localagi.NewClient(localagiURL, "", 5*time.Minute)
|
|
err := client.DeleteAgent("testagent1")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
|
|
It("create agent", func() {
|
|
client := localagi.NewClient(localagiURL, "", 5*time.Minute)
|
|
|
|
err := client.CreateAgent(&localagi.AgentConfig{
|
|
Name: "testagent1",
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
result, err := client.SimpleAIResponse("testagent1", "hello")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
Expect(result).ToNot(BeEmpty())
|
|
})
|
|
})
|
|
|
|
Context("Can do user defined tool calls", Ordered, Label("E2E"), func() {
|
|
BeforeAll(func() {
|
|
Eventually(func() error {
|
|
// test apiURL is working and available
|
|
_, err := http.Get(localagiURL + "/readyz")
|
|
return err
|
|
}, "10m", "10s").ShouldNot(HaveOccurred())
|
|
|
|
client := localagi.NewClient(localagiURL, "", time.Minute)
|
|
err := client.DeleteAgent("testagent2")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
err = client.CreateAgent(&localagi.AgentConfig{
|
|
Name: "testagent2",
|
|
EnableReasoning: true,
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
|
|
It("can create a task", func() {
|
|
client := localagi.NewClient(localagiURL, "", 10*time.Minute)
|
|
|
|
req := localagi.RequestBody{
|
|
Model: "testagent2",
|
|
Input: "Create an appointment next week on wednesday at 10:00 am for the whole day. The topic is about AI and you include ABC and DEF to the appointment.",
|
|
Tools: []localagi.Tool{
|
|
{
|
|
Type: "function",
|
|
Name: ptr.To("CreateTask"),
|
|
Description: ptr.To("Write the needed details whenever you're asked to create something like an info, appointment, e-mail or when you're asked to remind of anything or create a remainder. Also use this if you're supposed to answer an e-mail."),
|
|
Parameters: ptr.To(jsonschema.Definition{
|
|
Type: "object",
|
|
Properties: map[string]jsonschema.Definition{
|
|
"task": {
|
|
Type: "string",
|
|
Description: "Look for the name of the task you're supposed to do or create ",
|
|
Enum: []string{
|
|
"appointment",
|
|
"E-mail",
|
|
},
|
|
},
|
|
"subject": {
|
|
Type: "string",
|
|
Description: "A subject the task is about. Infer this from the given context data and user prompt.",
|
|
},
|
|
"reply": {
|
|
Type: "string",
|
|
Description: "A sharp and short reply to the contextual data given. Use a friendly and neutral general greeting.",
|
|
},
|
|
"recipient": {
|
|
Type: "array",
|
|
Description: "A list of names and abbreviations to send our task to. Abbreviations always have to match exactly. If the user gives you first names you can deduce the last name.",
|
|
Items: &jsonschema.Definition{
|
|
Type: "string",
|
|
Enum: []string{
|
|
"ABC",
|
|
"DEF",
|
|
},
|
|
},
|
|
},
|
|
"datestart": {
|
|
Type: "string",
|
|
Description: "The date and time when the task should start. Discard any older dates than today. Use tomorrow as default. Use the format DD/MM/YYYY HH:MM",
|
|
},
|
|
"dateend": {
|
|
Type: "string",
|
|
Description: "The date and time when a meeting should end. Default to start date. If the duration of an appointment is given, calculate the end with the start date. Use the format DD/MM/YYYY HH:MM",
|
|
},
|
|
"datedone": {
|
|
Type: "string",
|
|
Description: "The date and time when the task should be done. Use the format DD/MM/YYYY HH:MM",
|
|
},
|
|
"private": {
|
|
Type: "boolean",
|
|
Description: "Whether the task should be private or not. Default to false.",
|
|
},
|
|
"includeall": {
|
|
Type: "boolean",
|
|
Description: "Whether the task should include every mentioned person or not. Default to true. If you find explicitly mentioned people in the prompt whilst ignoring the contextual xml schema you choose false unless it is mentioned that you should include everyone.",
|
|
},
|
|
"wholedayappointment": {
|
|
Type: "boolean",
|
|
Description: "Whether the appointment should be done for the whole days. Default to false unless mentioned by the user prompt. Ignore the xml schema for this.",
|
|
},
|
|
"remainder": {
|
|
Type: "boolean",
|
|
Description: "Whether you are explicitly supposed to remind of something or not. Default to false. Ignore the xml schema for this.",
|
|
},
|
|
},
|
|
Required: []string{
|
|
"task",
|
|
"recipient",
|
|
"datestart",
|
|
"dateend",
|
|
"datedone",
|
|
"private",
|
|
"wholedayappointment",
|
|
"remainder",
|
|
"subject",
|
|
"reply",
|
|
"includeall",
|
|
},
|
|
}),
|
|
},
|
|
}}
|
|
result, err := client.GetAIResponse(&req)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(result).ToNot(BeNil())
|
|
|
|
var call localagi.ResponseFunctionToolCall
|
|
var args struct {
|
|
Task string `json:"task"`
|
|
Subject string `json:"subject"`
|
|
Reply string `json:"reply"`
|
|
Recipient []string `json:"recipient"`
|
|
DateStart string `json:"datestart"`
|
|
DateEnd string `json:"dateend"`
|
|
DateDone string `json:"datedone"`
|
|
Private bool `json:"private"`
|
|
IncludeAll bool `json:"includeall"`
|
|
WholeDayAppointment bool `json:"wholedayappointment"`
|
|
Remainder bool `json:"remainder"`
|
|
}
|
|
|
|
for _, out := range result.Output {
|
|
msg, err := out.ToMessage()
|
|
if err == nil && msg.Role == "assistant" {
|
|
xlog.Info("Agent returned message", "message", msg)
|
|
Expect(len(result.Output)).To(BeNumerically(">", 1), fmt.Sprintf("%+v", result.Output))
|
|
continue
|
|
}
|
|
fnc, err := out.ToFunctionToolCall()
|
|
call = fnc
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(string(fnc.Type)).To(Equal("function_call"))
|
|
Expect(fnc.Name).To(Equal("CreateTask"))
|
|
|
|
err = json.Unmarshal([]byte(fnc.Arguments), &args)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
Expect(args.Task).To(Equal("appointment"))
|
|
Expect(args.Subject).ToNot(BeEmpty())
|
|
Expect(args.Reply).ToNot(BeEmpty())
|
|
}
|
|
|
|
req = localagi.RequestBody{
|
|
Model: "testagent2",
|
|
Input: []any{
|
|
localagi.InputMessage{
|
|
Type: "message",
|
|
Role: "user",
|
|
Content: "Create an appointment next week on wednesday at 10:00 am for the whole day. The topic is about AI and you include ABC and DEF to the appointment.",
|
|
},
|
|
call,
|
|
localagi.InputFunctionToolCallOutput{
|
|
Type: "function_call_output",
|
|
CallID: call.CallID,
|
|
Output: fmt.Sprintf("Successfully created %s: %s", args.Task, args.Subject),
|
|
},
|
|
localagi.InputMessage{
|
|
Type: "message",
|
|
Role: "user",
|
|
Content: "Was the appointment created? Reply using the ChooseAnswer tool.",
|
|
},
|
|
},
|
|
Tools: []localagi.Tool{
|
|
{
|
|
Type: "function",
|
|
Name: ptr.To("ChooseAnswer"),
|
|
Description: ptr.To("Select Yes or No"),
|
|
Parameters: ptr.To(jsonschema.Definition{
|
|
Type: "object",
|
|
Properties: map[string]jsonschema.Definition{
|
|
"answer": {
|
|
Type: "boolean",
|
|
Description: "Set true for Yes and false for no",
|
|
},
|
|
},
|
|
Required: []string{
|
|
"answer",
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
ToolChoice: &localagi.ToolChoice{
|
|
Type: "function",
|
|
Name: "ChooseAnswer",
|
|
},
|
|
}
|
|
result, err = client.GetAIResponse(&req)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(len(result.Output)).To(BeNumerically(">", 0))
|
|
fnc, err := result.Output[len(result.Output)-1].ToFunctionToolCall()
|
|
Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("%+v", result))
|
|
Expect(fnc.Arguments).To(ContainSubstring("true"))
|
|
})
|
|
|
|
It("can tool call; web search", func() {
|
|
client := localagi.NewClient(localagiURL, "", 10*time.Minute)
|
|
|
|
req := localagi.RequestBody{
|
|
Model: "testagent2",
|
|
Input: "What's the weather like in Berlin?",
|
|
Tools: []localagi.Tool{
|
|
{
|
|
Type: "function",
|
|
Name: ptr.To("WebSearch"),
|
|
Description: ptr.To("Search the internet for up-to-date information"),
|
|
Parameters: ptr.To(jsonschema.Definition{
|
|
Type: "object",
|
|
Properties: map[string]jsonschema.Definition{
|
|
"SearchPrompt": {
|
|
Type: "string",
|
|
Description: "The query that is sent to a search engine",
|
|
},
|
|
},
|
|
Required: []string{
|
|
"SearchPrompt",
|
|
},
|
|
}),
|
|
},
|
|
}}
|
|
result, err := client.GetAIResponse(&req)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(result).ToNot(BeNil())
|
|
|
|
var call localagi.ResponseFunctionToolCall
|
|
var args struct {
|
|
SearchPrompt string `json:"SearchPrompt"`
|
|
}
|
|
|
|
for _, out := range result.Output {
|
|
msg, err := out.ToMessage()
|
|
if err == nil && msg.Role == "assistant" {
|
|
xlog.Info("Agent returned message", "message", msg)
|
|
continue
|
|
}
|
|
fnc, err := out.ToFunctionToolCall()
|
|
call = fnc
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(string(fnc.Type)).To(Equal("function_call"))
|
|
Expect(fnc.Name).To(Equal("WebSearch"))
|
|
|
|
err = json.Unmarshal([]byte(fnc.Arguments), &args)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
Expect(args.SearchPrompt).To(ContainSubstring("Berlin"))
|
|
}
|
|
|
|
req = localagi.RequestBody{
|
|
Model: "testagent2",
|
|
Input: []any{
|
|
localagi.InputMessage{
|
|
Type: "message",
|
|
Role: "user",
|
|
Content: "What's the weather like in Berlin compared to normal for Spring/Autumn?",
|
|
},
|
|
call,
|
|
localagi.InputFunctionToolCallOutput{
|
|
Type: "function_call_output",
|
|
CallID: call.CallID,
|
|
Output: "The current weather in Berlin: temp 30C!, heavy rain with risk of flooding, light wind, Humidity 90%, Pressure 900mb, visibility poor",
|
|
},
|
|
},
|
|
Tools: []localagi.Tool{
|
|
{
|
|
Type: "function",
|
|
Name: ptr.To("ChooseAnswer"),
|
|
Description: ptr.To("Select a multiple choice answer from a predefined list"),
|
|
Parameters: ptr.To(jsonschema.Definition{
|
|
Type: "object",
|
|
Properties: map[string]jsonschema.Definition{
|
|
"answer": {
|
|
Type: "string",
|
|
Description: "Choice of answer",
|
|
Enum: []string{
|
|
"Very hot and wet",
|
|
"Very hot and dry",
|
|
"About Average temp and very wet",
|
|
"About Average",
|
|
"About Average temp and very dry",
|
|
"Very cold and wet",
|
|
"Very cold and dry",
|
|
},
|
|
},
|
|
},
|
|
Required: []string{
|
|
"answer",
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
ToolChoice: &localagi.ToolChoice{
|
|
Type: "function",
|
|
Name: "ChooseAnswer",
|
|
},
|
|
}
|
|
result, err = client.GetAIResponse(&req)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(len(result.Output)).To(BeNumerically(">", 0))
|
|
fnc, err := result.Output[len(result.Output)-1].ToFunctionToolCall()
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(fnc.Arguments).To(ContainSubstring("Very hot and wet"))
|
|
})
|
|
|
|
})
|
|
})
|