From 0e5d37666f92bc75f12f118fc77a7e4af4a5924b Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Tue, 3 Mar 2026 11:46:03 +0300 Subject: [PATCH] Enha: id for card map --- bot.go | 4 ++-- helpfuncs.go | 20 ++++++++++---------- models/card.go | 12 +++++++++++- pngmeta/metareader.go | 6 ++++++ tables.go | 33 ++++++++++++--------------------- tools.go | 20 ++++++++++++++++---- 6 files changed, 57 insertions(+), 38 deletions(-) diff --git a/bot.go b/bot.go index 56a5318..75444e4 100644 --- a/bot.go +++ b/bot.go @@ -1382,8 +1382,8 @@ func applyCharCard(cc *models.CharCard, loadHistory bool) { } func charToStart(agentName string, keepSysP bool) bool { - cc, ok := sysMap[agentName] - if !ok { + cc := GetCardByRole(agentName) + if cc == nil { return false } applyCharCard(cc, keepSysP) diff --git a/helpfuncs.go b/helpfuncs.go index c407465..dab6b61 100644 --- a/helpfuncs.go +++ b/helpfuncs.go @@ -198,7 +198,11 @@ func initSysCards() ([]string, error) { logger.Warn("empty role", "file", cc.FilePath) continue } - sysMap[cc.Role] = cc + if cc.ID == "" { + cc.ID = models.ComputeCardID(cc.Role, cc.FilePath) + } + sysMap[cc.ID] = cc + roleToID[cc.Role] = cc.ID labels = append(labels, cc.Role) } return labels, nil @@ -289,8 +293,8 @@ func listRolesWithUser() []string { func loadImage() { filepath := defaultImage - cc, ok := sysMap[cfg.AssistantRole] - if ok { + cc := GetCardByRole(cfg.AssistantRole) + if cc != nil { if strings.HasSuffix(cc.FilePath, ".png") { filepath = cc.FilePath } @@ -468,13 +472,9 @@ func listChatRoles() []string { if !ok { return cbc } - currentCard, ok := sysMap[currentChat.Agent] - if !ok { - // case which won't let to switch roles: - // started new chat (basic_sys or any other), at the start it yet be saved or have chatbody - // if it does not have a card or chars, it'll return an empty slice - // log error - logger.Warn("failed to find current card in sysMap", "agent", currentChat.Agent, "sysMap", sysMap) + currentCard := GetCardByRole(currentChat.Agent) + if currentCard == nil { + logger.Warn("failed to find current card", "agent", currentChat.Agent) return cbc } charset := []string{} diff --git a/models/card.go b/models/card.go index 9bf6665..0bf437c 100644 --- a/models/card.go +++ b/models/card.go @@ -1,6 +1,10 @@ package models -import "strings" +import ( + "crypto/md5" + "fmt" + "strings" +) // https://github.com/malfoyslastname/character-card-spec-v2/blob/main/spec_v2.md // what a bloat; trim to Role->Msg pair and first msg @@ -31,6 +35,7 @@ func (c *CharCardSpec) Simplify(userName, fpath string) *CharCard { fm := strings.ReplaceAll(strings.ReplaceAll(c.FirstMes, "{{char}}", c.Name), "{{user}}", userName) sysPr := strings.ReplaceAll(strings.ReplaceAll(c.Description, "{{char}}", c.Name), "{{user}}", userName) return &CharCard{ + ID: ComputeCardID(c.Name, fpath), SysPrompt: sysPr, FirstMsg: fm, Role: c.Name, @@ -39,7 +44,12 @@ func (c *CharCardSpec) Simplify(userName, fpath string) *CharCard { } } +func ComputeCardID(role, filePath string) string { + return fmt.Sprintf("%x", md5.Sum([]byte(role+filePath))) +} + type CharCard struct { + ID string `json:"id"` SysPrompt string `json:"sys_prompt"` FirstMsg string `json:"first_msg"` Role string `json:"role"` diff --git a/pngmeta/metareader.go b/pngmeta/metareader.go index 7053546..e1835f9 100644 --- a/pngmeta/metareader.go +++ b/pngmeta/metareader.go @@ -109,6 +109,12 @@ func ReadCardJson(fname string) (*models.CharCard, error) { if err := json.Unmarshal(data, &card); err != nil { return nil, err } + if card.FilePath == "" { + card.FilePath = fname + } + if card.ID == "" { + card.ID = models.ComputeCardID(card.Role, card.FilePath) + } return &card, nil } diff --git a/tables.go b/tables.go index 5991aab..0cec551 100644 --- a/tables.go +++ b/tables.go @@ -159,27 +159,18 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table { // save updated card fi := strings.Index(selectedChat, "_") agentName := selectedChat[fi+1:] - cc, ok := sysMap[agentName] - if !ok { + cc := GetCardByRole(agentName) + if cc == nil { logger.Warn("no such card", "agent", agentName) - //no:lint if err := notifyUser("error", "no such card: "+agentName); err != nil { logger.Warn("failed ot notify", "error", err) } return } - // if chatBody.Messages[0].Role != "system" || chatBody.Messages[1].Role != agentName { - // if err := notifyUser("error", "unexpected chat structure; card: "+agentName); err != nil { - // logger.Warn("failed ot notify", "error", err) - // } - // return - // } - // change sys_prompt + first msg cc.SysPrompt = chatBody.Messages[0].Content cc.FirstMsg = chatBody.Messages[1].Content if err := pngmeta.WriteToPng(cc.ToSpec(cfg.UserRole), cc.FilePath, cc.FilePath); err != nil { - logger.Error("failed to write charcard", - "error", err) + logger.Error("failed to write charcard", "error", err) } return case "move sysprompt onto 1st msg": @@ -190,18 +181,16 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table { pages.RemovePage(historyPage) return case "new_chat_from_card": - // Reread card from file and start fresh chat fi := strings.Index(selectedChat, "_") agentName := selectedChat[fi+1:] - cc, ok := sysMap[agentName] - if !ok { + cc := GetCardByRole(agentName) + if cc == nil { logger.Warn("no such card", "agent", agentName) if err := notifyUser("error", "no such card: "+agentName); err != nil { logger.Warn("failed to notify", "error", err) } return } - // Reload card from disk newCard, err := pngmeta.ReadCard(cc.FilePath, cfg.UserRole) if err != nil { logger.Error("failed to reload charcard", "path", cc.FilePath, "error", err) @@ -214,9 +203,11 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table { return } } - // Update sysMap with fresh card data - sysMap[agentName] = newCard - // fetching sysprompt and first message anew from the card + if newCard.ID == "" { + newCard.ID = models.ComputeCardID(newCard.Role, newCard.FilePath) + } + sysMap[newCard.ID] = newCard + roleToID[newCard.Role] = newCard.ID startNewChat(false) pages.RemovePage(historyPage) return @@ -529,8 +520,8 @@ func makeAgentTable(agentList []string) *tview.Table { SetSelectable(false)) case 1: if actions[c-1] == "filepath" { - cc, ok := sysMap[agentList[r]] - if !ok { + cc := GetCardByRole(agentList[r]) + if cc == nil { continue } chatActTable.SetCell(r, c, diff --git a/tools.go b/tools.go index 3974a34..dfa8d7b 100644 --- a/tools.go +++ b/tools.go @@ -162,13 +162,15 @@ After that you are free to respond to the user. readURLSysPrompt = `Extract and summarize the content from the webpage. Provide key information, main points, and any relevant details.` summarySysPrompt = `Please provide a concise summary of the following conversation. Focus on key points, decisions, and actions. Provide only the summary, no additional commentary.` basicCard = &models.CharCard{ + ID: models.ComputeCardID("assistant", "basic_sys"), SysPrompt: basicSysMsg, FirstMsg: defaultFirstMsg, - Role: "", - FilePath: "", + Role: "assistant", + FilePath: "basic_sys", } - sysMap = map[string]*models.CharCard{"basic_sys": basicCard} - sysLabels = []string{"basic_sys"} + sysMap = map[string]*models.CharCard{} + roleToID = map[string]string{} + sysLabels = []string{"assistant"} webAgentClient *agent.AgentClient webAgentClientOnce sync.Once @@ -206,6 +208,8 @@ var ( ) func init() { + sysMap[basicCard.ID] = basicCard + roleToID["assistant"] = basicCard.ID sa, err := searcher.NewWebSurfer(searcher.SearcherTypeScraper, "") if err != nil { panic("failed to init seachagent; error: " + err.Error()) @@ -218,6 +222,14 @@ func init() { registerWindowTools() } +func GetCardByRole(role string) *models.CharCard { + cardID, ok := roleToID[role] + if !ok { + return nil + } + return sysMap[cardID] +} + func checkWindowTools() { xdotoolPath, _ = exec.LookPath("xdotool") maimPath, _ = exec.LookPath("maim")