From 7adcce78bef12e3edaa9c3167ffb12c358011186 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Mon, 8 Apr 2024 00:35:14 +0200 Subject: [PATCH] wip: UI --- example/webui/agentpool.go | 107 ++++++++++++---- example/webui/chat.html | 6 +- example/webui/create.html | 44 +++++-- example/webui/index.html | 8 +- example/webui/main.go | 254 ++++++++++++++++++------------------- example/webui/sse.go | 6 +- go.mod | 3 + go.sum | 6 + 8 files changed, 262 insertions(+), 172 deletions(-) diff --git a/example/webui/agentpool.go b/example/webui/agentpool.go index ff0d638..8bb0a6f 100644 --- a/example/webui/agentpool.go +++ b/example/webui/agentpool.go @@ -4,8 +4,11 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" + "time" . "github.com/mudler/local-agent-framework/agent" + "github.com/mudler/local-agent-framework/external" ) type ConnectorConfig struct { @@ -16,13 +19,11 @@ type ConnectorConfig struct { type ActionsConfig string type AgentConfig struct { - Connector []ConnectorConfig `json:"connector"` - Actions []ActionsConfig `json:"actions"` - StateFile string `json:"state_file"` - CharacterFile string `json:"character_file"` + Connector []ConnectorConfig `json:"connector"` + Actions []ActionsConfig `json:"actions"` // This is what needs to be part of ActionsConfig - APIURL string `json:"api_url"` Model string `json:"model"` + Name string `json:"name"` HUD bool `json:"hud"` Debug bool `json:"debug"` StandaloneJob bool `json:"standalone_job"` @@ -32,35 +33,47 @@ type AgentConfig struct { } type AgentPool struct { - file string - pool AgentPoolData - agents map[string]*Agent - managers map[string]Manager + file string + pooldir string + pool AgentPoolData + agents map[string]*Agent + managers map[string]Manager + apiURL, model string } type AgentPoolData map[string]AgentConfig -func NewAgentPool(file string) (*AgentPool, error) { +func NewAgentPool(model, apiURL, directory string) (*AgentPool, error) { // if file exists, try to load an existing pool. // if file does not exist, create a new pool. - if _, err := os.Stat(file); err != nil { + poolfile := filepath.Join(directory, "pool.json") + + if _, err := os.Stat(poolfile); err != nil { // file does not exist, create a new pool return &AgentPool{ - file: file, - agents: make(map[string]*Agent), - pool: make(map[string]AgentConfig), + file: poolfile, + pooldir: directory, + apiURL: apiURL, + model: model, + agents: make(map[string]*Agent), + pool: make(map[string]AgentConfig), + managers: make(map[string]Manager), }, nil } - poolData, err := loadPoolFromFile(file) + poolData, err := loadPoolFromFile(poolfile) if err != nil { return nil, err } return &AgentPool{ - file: file, - agents: make(map[string]*Agent), - pool: *poolData, + file: poolfile, + apiURL: apiURL, + pooldir: directory, + model: model, + agents: make(map[string]*Agent), + managers: make(map[string]Manager), + pool: *poolData, }, nil } @@ -91,16 +104,48 @@ func (a *AgentPool) List() []string { return agents } +func (a *AgentConfig) availableActions() []Action { + actions := []Action{} + if len(a.Actions) == 0 { + // Return search as default + return []Action{external.NewSearch(3)} + } + for _, action := range a.Actions { + switch action { + case "search": + actions = append(actions, external.NewSearch(3)) + } + } + + return actions +} + func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error { manager := NewManager(5) + model := a.model + if config.Model != "" { + model = config.Model + } + if config.PeriodicRuns == "" { + config.PeriodicRuns = "10m" + } + fmt.Println("Creating agent", name) + fmt.Println("Model", model) + fmt.Println("API URL", a.apiURL) + + actions := config.availableActions() opts := []Option{ - WithModel(config.Model), - WithLLMAPIURL(config.APIURL), + WithModel(model), + WithLLMAPIURL(a.apiURL), WithPeriodicRuns(config.PeriodicRuns), - WithStateFile(config.StateFile), - WithCharacterFile(config.StateFile), + WithActions( + actions..., + ), + WithStateFile(filepath.Join(a.pooldir, fmt.Sprintf("%s.state.json", name))), + WithCharacterFile(filepath.Join(a.pooldir, fmt.Sprintf("%s.character.json", name))), WithAgentReasoningCallback(func(state ActionCurrentState) bool { - sseManager.Send( + fmt.Println("Reasoning", state.Reasoning) + manager.Send( NewMessage( fmt.Sprintf(`Thinking: %s`, htmlIfy(state.Reasoning)), ).WithEvent("status"), @@ -108,6 +153,8 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error return true }), WithAgentResultCallback(func(state ActionState) { + fmt.Println("Reasoning", state.Reasoning) + text := fmt.Sprintf(`Reasoning: %s Action taken: %+v Parameters: %+v @@ -116,7 +163,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error state.ActionCurrentState.Action.Definition().Name, state.ActionCurrentState.Params, state.Result) - sseManager.Send( + manager.Send( NewMessage( htmlIfy( text, @@ -141,6 +188,9 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error opts = append(opts, WithRandomIdentity()) } } + + fmt.Println("Starting agent", name) + fmt.Printf("Config %+v\n", config) agent, err := New(opts...) if err != nil { return err @@ -155,6 +205,15 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error } }() + go func() { + for { + time.Sleep(1 * time.Second) // Send a message every seconds + manager.Send(NewMessage( + htmlIfy(agent.State().String()), + ).WithEvent("hud")) + } + }() + return nil } diff --git a/example/webui/chat.html b/example/webui/chat.html index 360b14a..b2fa760 100644 --- a/example/webui/chat.html +++ b/example/webui/chat.html @@ -42,11 +42,11 @@ } - +
-

Talk to '{{.Character.Name}}'

+

Talk to '{{.Name}}'

@@ -82,7 +82,7 @@
-
diff --git a/example/webui/create.html b/example/webui/create.html index bbb3798..1e1fef8 100644 --- a/example/webui/create.html +++ b/example/webui/create.html @@ -8,18 +8,44 @@

Create New Agent

-
+
- - + +
- - + + +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
diff --git a/example/webui/main.go b/example/webui/main.go index 5d65d86..f41fef5 100644 --- a/example/webui/main.go +++ b/example/webui/main.go @@ -2,19 +2,14 @@ package main import ( "fmt" - "html/template" "log" "math/rand" "net/http" "os" "strings" - "time" "github.com/donseba/go-htmx" - "github.com/donseba/go-htmx/sse" - fiber "github.com/gofiber/fiber/v3" - external "github.com/mudler/local-agent-framework/external" - "github.com/valyala/fasthttp/fasthttpadaptor" + fiber "github.com/gofiber/fiber/v2" . "github.com/mudler/local-agent-framework/agent" ) @@ -22,21 +17,19 @@ import ( type ( App struct { htmx *htmx.HTMX + pool *AgentPool } ) -var ( - sseManager Manager -) var testModel = os.Getenv("TEST_MODEL") -var apiModel = os.Getenv("API_MODEL") +var apiURL = os.Getenv("API_URL") func init() { if testModel == "" { testModel = "hermes-2-pro-mistral" } - if apiModel == "" { - apiModel = "http://192.168.68.113:8080" + if apiURL == "" { + apiURL = "http://192.168.68.113:8080" } } @@ -46,79 +39,40 @@ func htmlIfy(s string) string { return s } -var agentInstance *Agent - func main() { - app := &App{ - htmx: htmx.New(), - } - - agent, err := New( - WithLLMAPIURL(apiModel), - WithModel(testModel), - EnableHUD, - DebugMode, - EnableStandaloneJob, - WithAgentReasoningCallback(func(state ActionCurrentState) bool { - sseManager.Send( - sse.NewMessage( - fmt.Sprintf(`Thinking: %s`, htmlIfy(state.Reasoning)), - ).WithEvent("status"), - ) - return true - }), - WithActions(external.NewSearch(3)), - WithAgentResultCallback(func(state ActionState) { - text := fmt.Sprintf(`Reasoning: %s - Action taken: %+v - Parameters: %+v - Result: %s`, - state.Reasoning, - state.ActionCurrentState.Action.Definition().Name, - state.ActionCurrentState.Params, - state.Result) - sseManager.Send( - sse.NewMessage( - htmlIfy( - text, - ), - ).WithEvent("status"), - ) - }), - WithRandomIdentity(), - WithPeriodicRuns("10m"), - //WithPermanentGoal("get the weather of all the cities in italy and store the results"), - ) + // current dir + cwd, err := os.Getwd() if err != nil { panic(err) } - go agent.Run() - defer agent.Stop() + os.MkdirAll(cwd+"/pool", 0755) - agentInstance = agent - sseManager = NewManager(5) + pool, err := NewAgentPool(testModel, apiURL, cwd+"/pool") + if err != nil { + panic(err) - go func() { - for { - clientsStr := "" - clients := sseManager.Clients() - for _, c := range clients { - clientsStr += c + ", " - } + } + app := &App{ + htmx: htmx.New(), + pool: pool, + } - time.Sleep(1 * time.Second) // Send a message every seconds - sseManager.Send(NewMessage(fmt.Sprintf("connected clients: %v", clientsStr)).WithEvent("clients")) - } - }() + if err := pool.StartAll(); err != nil { + panic(err) + } - go func() { - for { - time.Sleep(1 * time.Second) // Send a message every seconds - sseManager.Send(NewMessage( - htmlIfy(agent.State().String()), - ).WithEvent("hud")) - } - }() + // go func() { + // for { + // clientsStr := "" + // clients := sseManager.Clients() + // for _, c := range clients { + // clientsStr += c + ", " + // } + + // time.Sleep(1 * time.Second) // Send a message every seconds + // sseManager.Send(NewMessage(fmt.Sprintf("connected clients: %v", clientsStr)).WithEvent("clients")) + // } + // }() // Initialize a new Fiber app webapp := fiber.New() @@ -126,26 +80,41 @@ func main() { // Serve static files webapp.Static("/", "./public") - webapp.Get("/", func(c fiber.Ctx) error { + webapp.Get("/", func(c *fiber.Ctx) error { return c.Render("index.html", fiber.Map{ - "Title": "Hello, World!", + "Agents": pool.List(), }) }) - webapp.Get("/create", func(c fiber.Ctx) error { + webapp.Get("/create", func(c *fiber.Ctx) error { return c.Render("create.html", fiber.Map{ "Title": "Hello, World!", }) }) // Define a route for the GET method on the root path '/' - webapp.Get("/sse", func(c fiber.Ctx) error { - sseManager.Handle(c, NewClient(randStringRunes(10))) + webapp.Get("/sse/:name", func(c *fiber.Ctx) error { + + m := pool.GetManager(c.Params("name")) + if m == nil { + return c.SendStatus(404) + } + + m.Handle(c, NewClient(randStringRunes(10))) return nil }) - webapp.Get("/notify", wrapHandler(http.HandlerFunc(app.Notify))) - webapp.Post("/chat", wrapHandler(http.HandlerFunc(app.Chat(sseManager)))) - webapp.Get("/talk", wrapHandler(http.HandlerFunc(app.Home(agent)))) + + webapp.Get("/notify/:name", app.Notify(pool)) + webapp.Post("/chat/:name", app.Chat(pool)) + webapp.Post("/create", app.Create(pool)) + + webapp.Get("/talk/:name", func(c *fiber.Ctx) error { + return c.Render("chat.html", fiber.Map{ + // "Character": agent.Character, + "Name": c.Params("name"), + }) + }) + log.Fatal(webapp.Listen(":3000")) // mux := http.NewServeMux() @@ -166,71 +135,94 @@ func main() { // log.Fatal(err) } -func (a *App) Home(agent *Agent) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - tmpl, err := template.ParseFiles("chat.html") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - tmpl.Execute(w, - struct { - Character Character - }{ - Character: agent.Character, - }) - } -} - // func (a *App) SSE(w http.ResponseWriter, r *http.Request) { // cl := sse.NewClient(randStringRunes(10)) // sseManager.Handle(w, r, cl) // } -func (a *App) Notify(w http.ResponseWriter, r *http.Request) { - query := strings.ToLower(r.PostFormValue("message")) - if query == "" { - _, _ = w.Write([]byte("Please enter a message.")) - return - } +func (a *App) Notify(pool *AgentPool) func(c *fiber.Ctx) error { + return func(c *fiber.Ctx) error { + payload := struct { + Message string `json:"message"` + }{} - agentInstance.Ask( - WithText(query), - ) - _, _ = w.Write([]byte("Message sent")) -} + if err := c.BodyParser(&payload); err != nil { + return err + } + + query := payload.Message + if query == "" { + _, _ = c.Write([]byte("Please enter a message.")) + return nil + } + + agent := pool.GetAgent(c.Params("name")) + agent.Ask( + WithText(query), + ) + _, _ = c.Write([]byte("Message sent")) -func wrapHandler(f func(http.ResponseWriter, *http.Request)) func(ctx fiber.Ctx) error { - return func(ctx fiber.Ctx) error { - fasthttpadaptor.NewFastHTTPHandler(http.HandlerFunc(f))(ctx.Context()) return nil } } -func (a *App) Chat(m Manager) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - query := strings.ToLower(r.PostFormValue("message")) - if query == "" { - _, _ = w.Write([]byte("Please enter a message.")) - return +func (a *App) Create(pool *AgentPool) func(c *fiber.Ctx) error { + return func(c *fiber.Ctx) error { + config := AgentConfig{} + if err := c.BodyParser(&config); err != nil { + return err } - m.Send( + + if config.Name == "" { + c.Status(http.StatusBadRequest).SendString("Name is required") + return nil + } + if err := pool.CreateAgent(config.Name, &config); err != nil { + c.Status(http.StatusInternalServerError).SendString(err.Error()) + return nil + } + return c.Redirect("/") + } +} + +func (a *App) Chat(pool *AgentPool) func(c *fiber.Ctx) error { + return func(c *fiber.Ctx) error { + payload := struct { + Message string `json:"message"` + }{} + + if err := c.BodyParser(&payload); err != nil { + return err + } + agentName := c.Params("name") + manager := pool.GetManager(agentName) + + query := payload.Message + if query == "" { + _, _ = c.Write([]byte("Please enter a message.")) + return nil + } + manager.Send( NewMessage( chatDiv(query, "gray"), ).WithEvent("messages")) go func() { - res := agentInstance.Ask( + agent := pool.GetAgent(agentName) + if agent == nil { + fmt.Println("Agent not found in pool", c.Params("name")) + return + } + res := agent.Ask( WithText(query), ) fmt.Println("response is", res.Response) - m.Send( - sse.NewMessage( + manager.Send( + NewMessage( chatDiv(res.Response, "blue"), ).WithEvent("messages")) - m.Send( - sse.NewMessage( + manager.Send( + NewMessage( inputMessageDisabled(false), // show again the input ).WithEvent("message_status")) @@ -238,10 +230,12 @@ func (a *App) Chat(m Manager) func(w http.ResponseWriter, r *http.Request) { // _, _ = w.Write([]byte(result)) }() - m.Send( - sse.NewMessage( + manager.Send( + NewMessage( loader() + inputMessageDisabled(true), ).WithEvent("message_status")) + + return nil } } diff --git a/example/webui/sse.go b/example/webui/sse.go index 391d65b..eab37be 100644 --- a/example/webui/sse.go +++ b/example/webui/sse.go @@ -7,7 +7,7 @@ import ( "sync" "time" - "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v2" "github.com/valyala/fasthttp" ) @@ -26,7 +26,7 @@ type ( // Manager defines the interface for managing clients and broadcasting messages. Manager interface { Send(message Envelope) - Handle(ctx fiber.Ctx, cl Listener) + Handle(ctx *fiber.Ctx, cl Listener) Clients() []string } @@ -111,7 +111,7 @@ func (manager *broadcastManager) Send(message Envelope) { } // Handle sets up a new client and handles the connection. -func (manager *broadcastManager) Handle(c fiber.Ctx, cl Listener) { +func (manager *broadcastManager) Handle(c *fiber.Ctx, cl Listener) { manager.register(cl) ctx := c.Context() diff --git a/go.mod b/go.mod index 1b6b46f..bd1e9b0 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/andybalholm/cascadia v1.1.0 // indirect github.com/go-logr/logr v1.3.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/gofiber/fiber/v2 v2.52.4 // indirect github.com/gofiber/utils/v2 v2.0.0-beta.4 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect @@ -27,6 +28,8 @@ require ( github.com/klauspost/compress v1.17.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect golang.org/x/net v0.21.0 // indirect diff --git a/go.sum b/go.sum index 08547df..2891efb 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM= +github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/gofiber/fiber/v3 v3.0.0-20240405062939-c8c51ee78331 h1:kDxTNPKMIRz8q28+tJHL2p87Cjtmkfn/OsLfastmpaY= github.com/gofiber/fiber/v3 v3.0.0-20240405062939-c8c51ee78331/go.mod h1:w7sdfTY0okjZ1oVH6rSOGvuACUIt0By1iK0HKUb3uqM= github.com/gofiber/utils/v2 v2.0.0-beta.4 h1:1gjbVFFwVwUb9arPcqiB6iEjHBwo7cHsyS41NeIW3co= @@ -36,12 +38,16 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo= github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/sap-nocops/duckduckgogo v0.0.0-20201102135645-176990152850 h1:DsVS3HK/t9X7ereJYMTiOeFSJWLOmrSG74CQhk2SlEs= github.com/sap-nocops/duckduckgogo v0.0.0-20201102135645-176990152850/go.mod h1:ur7dCshjxoPKHtsZgtb6n5gpOmzQNRQ5AT+yOLwaJxM= github.com/sashabaranov/go-openai v1.18.3 h1:dspFGkmZbhjg1059KhqLYSV2GaCiRIn+bOu50TlXUq8=