summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGrail Finder <wohilas@gmail.com>2024-12-09 19:26:26 +0300
committerGrail Finder <wohilas@gmail.com>2024-12-09 19:26:26 +0300
commit67f36d417fa97c4087e326623508370f60d3d3b8 (patch)
tree1a4466737b8bd854238d508d48945bde84ad0e31
parentbdd40ea8df60b6b161da3c1d201e9ec05ef743d1 (diff)
Feat: load char/agent; agent-chat flow
-rw-r--r--README.md9
-rw-r--r--bot.go37
-rw-r--r--session.go18
-rw-r--r--storage/storage.go8
-rw-r--r--tui.go87
5 files changed, 119 insertions, 40 deletions
diff --git a/README.md b/README.md
index f03c6bd..d9598ba 100644
--- a/README.md
+++ b/README.md
@@ -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;
diff --git a/bot.go b/bot.go
index 210a8b4..eb10e1d 100644
--- a/bot.go
+++ b/bot.go
@@ -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 {
diff --git a/session.go b/session.go
index c3d826f..21b5b9e 100644
--- a/session.go
+++ b/session.go
@@ -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 := `
diff --git a/tui.go b/tui.go
index 69279d7..fd1946a 100644
--- a/tui.go
+++ b/tui.go
@@ -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