Files
LocalAGI/core/agent/agent_test.go
Ettore Di Giacinto c23e655f44 feat(agent): shared state, allow to track conversations globally (#148)
* feat(agent): shared state, allow to track conversations globally

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Cleanup

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* track conversations initiated by the bot

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-05-11 22:23:01 +02:00

357 lines
9.8 KiB
Go

package agent_test
import (
"context"
"fmt"
"net/http"
"strings"
"sync"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/LocalAGI/services/actions"
. "github.com/mudler/LocalAGI/core/agent"
"github.com/mudler/LocalAGI/core/types"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/jsonschema"
)
const testActionResult = "In Boston it's 30C today, it's sunny, and humidity is at 98%"
const testActionResult2 = "In milan it's very hot today, it is 45C and the humidity is at 200%"
const testActionResult3 = "In paris it's very cold today, it is 2C and the humidity is at 10%"
var _ types.Action = &TestAction{}
var debugOptions = []types.JobOption{
types.WithReasoningCallback(func(state types.ActionCurrentState) bool {
xlog.Info("Reasoning", state)
return true
}),
types.WithResultCallback(func(state types.ActionState) {
xlog.Info("Reasoning", state.Reasoning)
xlog.Info("Action", state.Action)
xlog.Info("Result", state.Result)
}),
}
type TestAction struct {
response map[string]string
}
func (a *TestAction) Plannable() bool {
return true
}
func (a *TestAction) Run(c context.Context, sharedState *types.AgentSharedState, p types.ActionParams) (types.ActionResult, error) {
for k, r := range a.response {
if strings.Contains(strings.ToLower(p.String()), strings.ToLower(k)) {
return types.ActionResult{Result: r}, nil
}
}
return types.ActionResult{Result: "No match"}, nil
}
func (a *TestAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: "get_weather",
Description: "get current weather",
Properties: map[string]jsonschema.Definition{
"location": {
Type: jsonschema.String,
Description: "The city and state, e.g. San Francisco, CA",
},
"unit": {
Type: jsonschema.String,
Enum: []string{"celsius", "fahrenheit"},
},
},
Required: []string{"location"},
}
}
type FakeStoreResultAction struct {
TestAction
}
func (a *FakeStoreResultAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: "store_results",
Description: "store results permanently. Use this tool after you have a result you want to keep.",
Properties: map[string]jsonschema.Definition{
"term": {
Type: jsonschema.String,
Description: "What to store permanently",
},
},
Required: []string{"term"},
}
}
type FakeInternetAction struct {
TestAction
}
func (a *FakeInternetAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: "search_internet",
Description: "search on internet",
Properties: map[string]jsonschema.Definition{
"term": {
Type: jsonschema.String,
Description: "What to search for",
},
},
Required: []string{"term"},
}
}
var _ = Describe("Agent test", func() {
Context("jobs", func() {
BeforeEach(func() {
Eventually(func() error {
// test apiURL is working and available
_, err := http.Get(apiURL + "/readyz")
return err
}, "10m", "10s").ShouldNot(HaveOccurred())
})
It("pick the correct action", func() {
agent, err := New(
WithLLMAPIURL(apiURL),
WithModel(testModel),
EnableForceReasoning,
WithTimeout("10m"),
WithLoopDetectionSteps(3),
// WithRandomIdentity(),
WithActions(&TestAction{response: map[string]string{
"boston": testActionResult,
"milan": testActionResult2,
"paris": testActionResult3,
}}),
)
Expect(err).ToNot(HaveOccurred())
go agent.Run()
defer agent.Stop()
res := agent.Ask(
append(debugOptions,
types.WithText("what's the weather in Boston and Milano? Use celsius units"),
)...,
)
Expect(res.Error).ToNot(HaveOccurred())
reasons := []string{}
for _, r := range res.State {
reasons = append(reasons, r.Result)
}
Expect(reasons).To(ContainElement(testActionResult), fmt.Sprint(res))
Expect(reasons).To(ContainElement(testActionResult2), fmt.Sprint(res))
reasons = []string{}
res = agent.Ask(
append(debugOptions,
types.WithText("Now I want to know the weather in Paris, always use celsius units"),
)...)
for _, r := range res.State {
reasons = append(reasons, r.Result)
}
//Expect(reasons).ToNot(ContainElement(testActionResult), fmt.Sprint(res))
//Expect(reasons).ToNot(ContainElement(testActionResult2), fmt.Sprint(res))
Expect(reasons).To(ContainElement(testActionResult3), fmt.Sprint(res))
// conversation := agent.CurrentConversation()
// for _, r := range res.State {
// reasons = append(reasons, r.Result)
// }
// Expect(len(conversation)).To(Equal(10), fmt.Sprint(conversation))
})
It("pick the correct action", func() {
agent, err := New(
WithLLMAPIURL(apiURL),
WithModel(testModel),
WithTimeout("10m"),
// WithRandomIdentity(),
WithActions(&TestAction{response: map[string]string{
"boston": testActionResult,
},
}),
)
Expect(err).ToNot(HaveOccurred())
go agent.Run()
defer agent.Stop()
res := agent.Ask(
append(debugOptions,
types.WithText("can you get the weather in boston? Use celsius units"))...,
)
reasons := []string{}
for _, r := range res.State {
reasons = append(reasons, r.Result)
}
Expect(reasons).To(ContainElement(testActionResult), fmt.Sprint(res))
})
It("updates the state with internal actions", func() {
agent, err := New(
WithLLMAPIURL(apiURL),
WithModel(testModel),
WithTimeout("10m"),
EnableHUD,
// EnableStandaloneJob,
// WithRandomIdentity(),
WithPermanentGoal("I want to learn to play music"),
)
Expect(err).ToNot(HaveOccurred())
go agent.Run()
defer agent.Stop()
result := agent.Ask(
types.WithText("Update your goals such as you want to learn to play the guitar"),
)
fmt.Printf("%+v\n", result)
Expect(result.Error).ToNot(HaveOccurred())
Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State()))
})
It("Can generate a plan", func() {
agent, err := New(
WithLLMAPIURL(apiURL),
WithModel(testModel),
WithLLMAPIKey(apiKeyURL),
WithTimeout("10m"),
WithActions(
&TestAction{response: map[string]string{
"boston": testActionResult,
"milan": testActionResult2,
}},
),
EnablePlanning,
EnableForceReasoning,
// EnableStandaloneJob,
// WithRandomIdentity(),
)
Expect(err).ToNot(HaveOccurred())
go agent.Run()
defer agent.Stop()
result := agent.Ask(
types.WithText("Use the plan tool to do two actions in sequence: search for the weather in boston and search for the weather in milan"),
)
Expect(len(result.State)).To(BeNumerically(">", 1))
actionsExecuted := []string{}
actionResults := []string{}
for _, r := range result.State {
xlog.Info(r.Result)
actionsExecuted = append(actionsExecuted, r.Action.Definition().Name.String())
actionResults = append(actionResults, r.ActionResult.Result)
}
Expect(actionsExecuted).To(ContainElement("get_weather"), fmt.Sprint(result))
Expect(actionsExecuted).To(ContainElement("plan"), fmt.Sprint(result))
Expect(actionResults).To(ContainElement(testActionResult), fmt.Sprint(result))
Expect(actionResults).To(ContainElement(testActionResult2), fmt.Sprint(result))
})
It("Can initiate conversations", func() {
message := openai.ChatCompletionMessage{}
mu := &sync.Mutex{}
agent, err := New(
WithLLMAPIURL(apiURL),
WithModel(testModel),
WithLLMAPIKey(apiKeyURL),
WithTimeout("10m"),
WithNewConversationSubscriber(func(m openai.ChatCompletionMessage) {
mu.Lock()
message = m
mu.Unlock()
}),
WithActions(
actions.NewSearch(map[string]string{}),
),
EnablePlanning,
EnableForceReasoning,
EnableInitiateConversations,
EnableStandaloneJob,
EnableHUD,
WithPeriodicRuns("1s"),
WithPermanentGoal("use the new_conversation tool to initiate a conversation with the user"),
// EnableStandaloneJob,
// WithRandomIdentity(),
)
Expect(err).ToNot(HaveOccurred())
go agent.Run()
defer agent.Stop()
Eventually(func() string {
mu.Lock()
defer mu.Unlock()
return message.Content
}, "10m", "10s").ShouldNot(BeEmpty())
})
/*
It("it automatically performs things in the background", func() {
agent, err := New(
WithLLMAPIURL(apiURL),
WithModel(testModel),
EnableHUD,
EnableStandaloneJob,
WithAgentReasoningCallback(func(state ActionCurrentState) bool {
xlog.Info("Reasoning", state)
return true
}),
WithAgentResultCallback(func(state ActionState) {
xlog.Info("Reasoning", state.Reasoning)
xlog.Info("Action", state.Action)
xlog.Info("Result", state.Result)
}),
WithActions(
&FakeInternetAction{
TestAction{
response:
map[string]string{
"italy": "The weather in italy is sunny",
}
},
},
&FakeStoreResultAction{
TestAction{
response: []string{
"Result permanently stored",
},
},
},
),
//WithRandomIdentity(),
WithPermanentGoal("get the weather of all the cities in italy and store the results"),
)
Expect(err).ToNot(HaveOccurred())
go agent.Run()
defer agent.Stop()
Eventually(func() string {
return agent.State().Goal
}, "10m", "10s").Should(ContainSubstring("weather"), fmt.Sprint(agent.State()))
Eventually(func() string {
return agent.State().String()
}, "10m", "10s").Should(ContainSubstring("store"), fmt.Sprint(agent.State()))
// result := agent.Ask(
// WithText("Update your goals such as you want to learn to play the guitar"),
// )
// fmt.Printf("%+v\n", result)
// Expect(result.Error).ToNot(HaveOccurred())
// Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State()))
})
*/
})
})