diff options
author | Grail Finder <wohilas@gmail.com> | 2024-12-09 19:26:26 +0300 |
---|---|---|
committer | Grail Finder <wohilas@gmail.com> | 2024-12-09 19:26:26 +0300 |
commit | 67f36d417fa97c4087e326623508370f60d3d3b8 (patch) | |
tree | 1a4466737b8bd854238d508d48945bde84ad0e31 | |
parent | bdd40ea8df60b6b161da3c1d201e9ec05ef743d1 (diff) |
Feat: load char/agent; agent-chat flow
-rw-r--r-- | README.md | 9 | ||||
-rw-r--r-- | bot.go | 37 | ||||
-rw-r--r-- | session.go | 18 | ||||
-rw-r--r-- | storage/storage.go | 8 | ||||
-rw-r--r-- | tui.go | 87 |
5 files changed, 119 insertions, 40 deletions
@@ -19,11 +19,12 @@ - rename current chat; + - help page with all key bindings; + - default config file (api url, path to sysprompts, path to log, limits, etc); + +- ctrl+n to start new chat; + +- export whole chat into a json file; + +- directory with sys prompts (charcards png & json); + - change temp, min-p and other params from tui; - fullscreen textarea option (bothersome to implement); - consider adding use /completion of llamacpp, since openai endpoint clearly has template|format issues; -- export whole chat into a json file; -- directory with sys prompts (charcards png & json); - separate messages that are stored and chat and send to the bot, i.e. option to omit tool calls (there might be a point where they are no longer needed in ctx); - colourschemes, colours or markdown of quotes and styles; - RAG support|implementation; @@ -31,6 +32,7 @@ - char card is the sys message, but how about giving tools to char that does not have it? - it is a bit clumsy to mix chats in db and chars from the external files, maybe load external files in db on startup? - lets say we have two (or more) agents with the same name across multiple chats. These agents go and ask db for topics they memorised. Now they can access topics that aren't meant for them. (so memory should have an option: shareable; that indicates if that memory can be shared across chats); +- delete chat option; ### FIX: - bot responding (or hanging) blocks everything; + @@ -46,3 +48,6 @@ - when bot generation ended with err: need a way to switch back to the bot_resp_false mode; + - no selection focus on modal sys buttons after opening it a second time; (cannot reproduce) + - chat should contain char in it (one to many: char: []chats); + +- all page names should be vars; +- normal case regen omits assistant icon; +- user icon (and role?) from config is not used; @@ -13,6 +13,7 @@ import ( "net/http" "os" "strings" + "time" "github.com/rivo/tview" ) @@ -192,11 +193,39 @@ func chatToText(showSys bool) string { func applyCharCard(cc *models.CharCard) { cfg.AssistantRole = cc.Role - newChat := []models.RoleMsg{ - {Role: "system", Content: cc.SysPrompt}, - {Role: cfg.AssistantRole, Content: cc.FirstMsg}, + // try to load last active chat + history, err := loadAgentsLastChat(cfg.AssistantRole) + if err != nil { + logger.Warn("failed to load last agent chat;", "agent", cc.Role, "err", err) + history = []models.RoleMsg{ + {Role: "system", Content: cc.SysPrompt}, + {Role: cfg.AssistantRole, Content: cc.FirstMsg}, + } + id, err := store.ChatGetMaxID() + if err != nil { + logger.Error("failed to get max chat id from db;", "id:", id) + // INFO: will rewrite first chat + } + chat := &models.Chat{ + ID: id + 1, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Agent: cfg.AssistantRole, + } + chat.Name = fmt.Sprintf("%d_%s", chat.ID, cfg.AssistantRole) + chatMap[chat.Name] = chat + activeChatName = chat.Name + } + chatBody.Messages = history +} + +func charToStart(agentName string) bool { + cc, ok := sysMap[agentName] + if !ok { + return false } - chatBody.Messages = newChat + applyCharCard(cc) + return true } // func textToMsg(rawMsg string) models.RoleMsg { @@ -79,6 +79,24 @@ func loadHistoryChat(chatName string) ([]models.RoleMsg, error) { return chat.ToHistory() } +func loadAgentsLastChat(agent string) ([]models.RoleMsg, error) { + chat, err := store.GetLastChatByAgent(agent) + if err != nil { + return nil, err + } + history, err := chat.ToHistory() + if err != nil { + return nil, err + } + if chat.Name == "" { + logger.Warn("empty chat name", "id", chat.ID) + chat.Name = fmt.Sprintf("%s_%d", chat.Agent, chat.ID) + } + chatMap[chat.Name] = chat + activeChatName = chat.Name + return history, nil +} + func loadOldChatOrGetNew() []models.RoleMsg { newChat := &models.Chat{ ID: 0, diff --git a/storage/storage.go b/storage/storage.go index 66640fd..9a1595c 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -17,6 +17,7 @@ type ChatHistory interface { ListChats() ([]models.Chat, error) GetChatByID(id uint32) (*models.Chat, error) GetLastChat() (*models.Chat, error) + GetLastChatByAgent(agent string) (*models.Chat, error) UpsertChat(chat *models.Chat) (*models.Chat, error) RemoveChat(id uint32) error ChatGetMaxID() (uint32, error) @@ -45,6 +46,13 @@ func (p ProviderSQL) GetLastChat() (*models.Chat, error) { return &resp, err } +func (p ProviderSQL) GetLastChatByAgent(agent string) (*models.Chat, error) { + resp := models.Chat{} + query := "SELECT * FROM chats WHERE agent=$1 ORDER BY updated_at DESC LIMIT 1" + err := p.db.Get(&resp, query, agent) + return &resp, err +} + func (p ProviderSQL) UpsertChat(chat *models.Chat) (*models.Chat, error) { // Prepare the SQL statement query := ` @@ -36,15 +36,16 @@ var ( [yellow]F6[white]: interrupt bot resp [yellow]F7[white]: copy last msg to clipboard (linux xclip) [yellow]F8[white]: copy n msg to clipboard (linux xclip) -[yellow]Ctrl+s[white]: choose/replace system prompt +[yellow]Ctrl+s[white]: load new char/agent [yellow]Ctrl+e[white]: export chat to json file +[yellow]Ctrl+n[white]: start a new chat Press Enter to go back ` ) func colorText() { - // INFO: looks way too inefficient; use it with care or make it optional + // INFO: is there a better way to markdown? tv := textView.GetText(false) cq := quotesRE.ReplaceAllString(tv, `[orange:-:-]$1[-:-:-]`) textView.SetText(starRE.ReplaceAllString(cq, `[turquoise::i]$1[-:-:-]`)) @@ -54,6 +55,46 @@ func updateStatusLine() { position.SetText(fmt.Sprintf(indexLine, botRespMode, cfg.AssistantRole, activeChatName)) } +func initSysCards() ([]string, error) { + labels := []string{} + labels = append(labels, sysLabels...) + cards, err := pngmeta.ReadDirCards(cfg.SysDir, cfg.UserRole) + if err != nil { + logger.Error("failed to read sys dir", "error", err) + return nil, err + } + for _, cc := range cards { + sysMap[cc.Role] = cc + labels = append(labels, cc.Role) + } + return labels, nil +} + +func startNewChat() { + id, err := store.ChatGetMaxID() + if err != nil { + logger.Error("failed to get chat id", "error", err) + } + // TODO: get the current agent and it's starter + if ok := charToStart(cfg.AssistantRole); !ok { + logger.Warn("no such sys msg", "name", cfg.AssistantRole) + } + // set chat body + chatBody.Messages = defaultStarter + textView.SetText(chatToText(cfg.ShowSys)) + newChat := &models.Chat{ + ID: id + 1, + Name: fmt.Sprintf("%v_%v", "new", time.Now().Unix()), + Msgs: string(defaultStarterBytes), + Agent: cfg.AssistantRole, + } + activeChatName = newChat.Name + chatMap[newChat.Name] = newChat + updateStatusLine() + colorText() + return +} + func init() { theme := tview.Theme{ PrimitiveBackgroundColor: tcell.ColorDefault, @@ -102,24 +143,8 @@ func init() { SetDoneFunc(func(buttonIndex int, buttonLabel string) { switch buttonLabel { case "new": - id, err := store.ChatGetMaxID() - if err != nil { - logger.Error("failed to get chat id", "error", err) - } - // set chat body - chatBody.Messages = defaultStarter - textView.SetText(chatToText(cfg.ShowSys)) - newChat := &models.Chat{ - ID: id + 1, - Name: fmt.Sprintf("%v_%v", "new", time.Now().Unix()), - Msgs: string(defaultStarterBytes), - Agent: cfg.AssistantRole, - } - // activeChatName = path.Join(historyDir, fmt.Sprintf("%d_chat.json", time.Now().Unix())) - activeChatName = newChat.Name - chatMap[newChat.Name] = newChat + startNewChat() pages.RemovePage("history") - colorText() return // set text case "cancel": @@ -155,21 +180,15 @@ func init() { sysModal.ClearButtons() return default: - cc, ok := sysMap[buttonLabel] - if !ok { + if ok := charToStart(buttonLabel); !ok { logger.Warn("no such sys msg", "name", buttonLabel) pages.RemovePage("sys") return } - // to replace it old role in text - // oldRole := chatBody.Messages[0].Role - // replace every role with char - // chatBody.Messages[0].Content = cc.SysPrompt - // chatBody.Messages[1].Content = cc.FirstMsg - applyCharCard(cc) // replace textview textView.SetText(chatToText(cfg.ShowSys)) colorText() + updateStatusLine() sysModal.ClearButtons() pages.RemovePage("sys") app.SetFocus(textArea) @@ -294,6 +313,7 @@ func init() { textView.SetText(chatToText(cfg.ShowSys)) colorText() textView.ScrollToEnd() + initSysCards() app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyF1 { chatList, err := loadHistoryChats() @@ -387,9 +407,13 @@ func init() { textArea.SetText("pressed ctrl+a", true) return nil } + if event.Key() == tcell.KeyCtrlN { + startNewChat() + return nil + } if event.Key() == tcell.KeyCtrlS { // switch sys prompt - cards, err := pngmeta.ReadDirCards(cfg.SysDir, cfg.UserRole) + labels, err := initSysCards() if err != nil { logger.Error("failed to read sys dir", "error", err) if err := notifyUser("error", "failed to read: "+cfg.SysDir); err != nil { @@ -397,15 +421,10 @@ func init() { } return nil } - labels := []string{} - labels = append(labels, sysLabels...) - for _, cc := range cards { - labels = append(labels, cc.Role) - sysMap[cc.Role] = cc - } sysModal.AddButtons(labels) // load all chars pages.AddPage("sys", sysModal, true, true) + updateStatusLine() return nil } // cannot send msg in editMode or botRespMode |