package main import ( "bytes" "embed" "encoding/json" "fmt" "html/template" "io/ioutil" "log" "net/http" "strings" "time" "git.riomhaire.com/gremlin/elizaservice/eliza" "git.riomhaire.com/gremlin/elizaservice/facades/cache" "git.riomhaire.com/gremlin/elizaservice/infrastructure" "github.com/gofrs/uuid" "github.com/jaffee/commandeer" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) //go:embed web/* var web embed.FS //go:embed templates/* var templates embed.FS //go:embed bots/* var bots embed.FS var cacheManager cache.BotCache var cacheTTL int /*********************************************************************************************** * ***********************************************************************************************/ type Main struct { BotName string `help:"What This Bot is Called."` Port int `help:"What port bot is listening too"` Cache string `help:"What Cache configuration to use - redis format ::"` CacheTTL int `help:"How long cache is allowed to store data in seconds"` } /*********************************************************************************************** * ***********************************************************************************************/ func NewMain() *Main { return &Main{ BotName: "Eliza", Port: 8070, Cache: "empire:6379:1", CacheTTL: 3600, } } /******************************** This appends to session in cache latest info *********************************/ func storeConversation(sessionID string, sessionData eliza.SessionData) { // Save it as json j, _ := json.Marshal(sessionData) cacheManager.Store(sessionID, string(j), cacheTTL) } /******************************** This retrieves the context from the cache *********************************/ func retrieveConversation(sessionID, bot, botversion, user string) (sessionData eliza.SessionData) { // Lookup session jsonValue, found, _ := cacheManager.Retrieve(sessionID, false) if !found { sessionData = eliza.SessionData{SessionID: sessionID, Bot: bot, BotVersion: botversion, User: user, StartTime: time.Now().UTC().String(), Conversation: make([]eliza.ChatbotInteraction, 0)} } if found { if err := json.Unmarshal([]byte(jsonValue), &sessionData); err != nil { sessionData = eliza.SessionData{SessionID: sessionID, Bot: bot, BotVersion: botversion, User: user, StartTime: time.Now().UTC().String(), Conversation: make([]eliza.ChatbotInteraction, 0)} } } return } /*********************************************************************************************** * This at the moment ... but it could be one passed in via LTI if available ***********************************************************************************************/ func createSession(c echo.Context) string { guid, _ := uuid.NewV4() sessionID := guid.String() return sessionID } /*********************************************************************************************** * ***********************************************************************************************/ func findPersonality(name string) (personality eliza.Personality, err error) { filename := "bots/" + name + ".json" jsonFile, err := bots.Open(filename) // if we os.Open returns an error then handle it if err != nil { return } // fmt.Println("Successfully Opened ", filename) byteValue, _ := ioutil.ReadAll(jsonFile) // we initialize our Users array personality = eliza.Personality{} // we unmarshal our byteArray which contains our // jsonFile's content into 'users' which we defined above json.Unmarshal(byteValue, &personality) // defer the closing of our jsonFile so that we can parse it later on defer jsonFile.Close() return } /*********************************************************************************************** * ***********************************************************************************************/ func chantboInteractiontEndpoint(c echo.Context) error { // Extract question from GET request user := c.QueryParam("user") question := c.QueryParam("value") botname := c.QueryParam("bot") sessionID := c.QueryParam("session") // default to eliza if len(botname) == 0 { botname = "eliza" } personality, err := findPersonality(strings.ToLower(botname)) // files are in lowercase if err != nil { return c.String(http.StatusOK, err.Error()) } // Restore Conversation Context sessionData := retrieveConversation(sessionID, botname, personality.Version, user) // Return Eliza's response's so long as the user doesn't give a quit statement chatboxContext := eliza.ChatbotContext{infrastructure.Version, sessionData} character := eliza.NewBotPersonality(&personality, &chatboxContext) answer := character.ReplyTo(question) msg := fmt.Sprintf("Bot [%s %s] Session ID [%s] Question [%s] Answer [%s]", botname, personality.Version, sessionID, question, answer) fmt.Println(msg) // update cache // OK addToConversation botInteraction := eliza.ChatbotInteraction{time.Now().UTC().String(), question, answer} sessionData.Conversation = append(sessionData.Conversation, botInteraction) storeConversation(sessionID, sessionData) // Return Eliza's answer return c.String(http.StatusOK, answer) } /*********************************************************************************************** * ***********************************************************************************************/ func chantbotEndpoint(c echo.Context) error { c.Response().WriteHeader(http.StatusOK) c.Response().Header().Add("Content-Type", "text/html") // Note the call to ParseFS instead of Parse personality := c.Param("personality") t, err := template.ParseFS(templates, fmt.Sprintf("templates/%s.html", personality)) if err != nil { log.Fatal(err) } sessionID := createSession(c) fmt.Println("Session ID [", sessionID, "] Created") var tBuffer bytes.Buffer data := struct { SessionID string Id string GivenName string FamilyName string Name string Bot string }{ SessionID: sessionID, Id: c.QueryParam("user_id"), GivenName: c.QueryParam("given_name"), FamilyName: c.QueryParam("family_name"), Name: c.QueryParam("name"), Bot: strings.Title(strings.ToLower(personality)), } // respond with the output of template execution t.Execute(&tBuffer, data) page := tBuffer.String() return c.String(http.StatusOK, page) } /*********************************************************************************************** * ***********************************************************************************************/ func (m *Main) Run() error { fmt.Println(` ███████╗██╗ ██╗███████╗ █████╗ ██╔════╝██║ ██║╚══███╔╝██╔══██╗ █████╗ ██║ ██║ ███╔╝ ███████║ ██╔══╝ ██║ ██║ ███╔╝ ██╔══██║ ███████╗███████╗██║███████╗██║ ██║ ╚══════╝╚══════╝╚═╝╚══════╝╚═╝ ╚═╝`) fmt.Println("Application Version : ", infrastructure.Version) // Create Cache Manager (redis) cacheManager = cache.NewRedisCache(m.Cache) cacheTTL = m.CacheTTL e := echo.New() e.Use(middleware.CORS()) var contentHandler = echo.WrapHandler(http.FileServer(http.FS(web))) var contentRewrite = middleware.Rewrite(map[string]string{"/*": "/web/$1"}) e.GET("/*", contentHandler, contentRewrite) e.GET("/user-input", chantboInteractiontEndpoint) e.GET("/chatbot/:personality", chantbotEndpoint) e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", m.Port))) return nil } /*********************************************************************************************** * ***********************************************************************************************/ func main() { err := commandeer.Run(NewMain()) if err != nil { fmt.Println(err) } }