Feat: edit agent png cards
This commit is contained in:
@@ -37,12 +37,12 @@
|
||||
- connection to a model status;
|
||||
- ===== /llamacpp specific (it has a different body -> interface instead of global var)
|
||||
- edit syscards / create new ones;
|
||||
- consider adding use /completion of llamacpp, since openai endpoint clearly has template|format issues;
|
||||
- consider adding use /completion of llamacpp, since openai endpoint clearly has template|format issues; +
|
||||
- change temp, min-p and other params from tui;
|
||||
- DRY;
|
||||
- DRY; +
|
||||
- keybind to switch between openai and llamacpp endpoints;
|
||||
- option to remove <thinking> from chat history;
|
||||
- in chat management table add preview of the last message;
|
||||
- in chat management table add preview of the last message; +
|
||||
|
||||
### FIX:
|
||||
- bot responding (or hanging) blocks everything; +
|
||||
@@ -67,3 +67,5 @@
|
||||
- F1 can load any chat, by loading chat of other agent it does not switch agents, if that chat is continued, it will rewrite agent in db; (either allow only chats from current agent OR switch agent on chat loading); +
|
||||
- after chat is deleted: load undeleted chat; +
|
||||
- name split for llamacpp completion. user msg should end with 'bot_name:';
|
||||
- add retry on failed call (and EOF);
|
||||
- model info shold be an event and show disconnect status when fails;
|
||||
|
||||
65
bot.go
65
bot.go
@@ -13,6 +13,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -43,32 +44,6 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// ====
|
||||
|
||||
// DEPRECATED
|
||||
// func formMsg(chatBody *models.ChatBody, newMsg, role string) io.Reader {
|
||||
// if newMsg != "" { // otherwise let the bot continue
|
||||
// newMsg := models.RoleMsg{Role: role, Content: newMsg}
|
||||
// chatBody.Messages = append(chatBody.Messages, newMsg)
|
||||
// // if rag
|
||||
// if cfg.RAGEnabled {
|
||||
// ragResp, err := chatRagUse(newMsg.Content)
|
||||
// if err != nil {
|
||||
// logger.Error("failed to form a rag msg", "error", err)
|
||||
// return nil
|
||||
// }
|
||||
// ragMsg := models.RoleMsg{Role: cfg.ToolRole, Content: ragResp}
|
||||
// chatBody.Messages = append(chatBody.Messages, ragMsg)
|
||||
// }
|
||||
// }
|
||||
// data, err := json.Marshal(chatBody)
|
||||
// if err != nil {
|
||||
// logger.Error("failed to form a msg", "error", err)
|
||||
// return nil
|
||||
// }
|
||||
// return bytes.NewReader(data)
|
||||
// }
|
||||
|
||||
func fetchModelName() {
|
||||
api := "http://localhost:8080/v1/models"
|
||||
resp, err := httpClient.Get(api)
|
||||
@@ -293,6 +268,42 @@ func chatToText(showSys bool) string {
|
||||
return strings.Join(s, "")
|
||||
}
|
||||
|
||||
// func removeThinking() {
|
||||
// s := chatToTextSlice(false) // will delete tools messages though
|
||||
// chat := strings.Join(s, "")
|
||||
// chat = thinkRE.ReplaceAllString(chat, "")
|
||||
// reS := fmt.Sprintf("[%s:\n,%s:\n]", cfg.AssistantRole, cfg.UserRole)
|
||||
// // no way to know what agent wrote which msg
|
||||
// s = regexp.MustCompile(reS).Split(chat, -1)
|
||||
// }
|
||||
|
||||
func textToMsgs(text string) []models.RoleMsg {
|
||||
lines := strings.Split(text, "\n")
|
||||
roleRE := regexp.MustCompile(`^\(\d+\) <.*>:`)
|
||||
resp := []models.RoleMsg{}
|
||||
oldrole := ""
|
||||
for _, line := range lines {
|
||||
if roleRE.MatchString(line) {
|
||||
// extract role
|
||||
role := ""
|
||||
// if role changes
|
||||
if role != oldrole {
|
||||
oldrole = role
|
||||
// newmsg
|
||||
msg := models.RoleMsg{
|
||||
Role: role,
|
||||
}
|
||||
resp = append(resp, msg)
|
||||
}
|
||||
resp[len(resp)-1].Content += "\n" + line
|
||||
}
|
||||
}
|
||||
if len(resp) != 0 {
|
||||
resp[0].Content = strings.TrimPrefix(resp[0].Content, "\n")
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func applyCharCard(cc *models.CharCard) {
|
||||
cfg.AssistantRole = cc.Role
|
||||
// TODO: need map role->icon
|
||||
@@ -381,6 +392,6 @@ func init() {
|
||||
Messages: lastChat,
|
||||
}
|
||||
initChunkParser()
|
||||
go runModelNameTicker(time.Second * 120)
|
||||
// go runModelNameTicker(time.Second * 120)
|
||||
// tempLoad()
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ type CharCardSpec struct {
|
||||
Spec string `json:"spec"`
|
||||
SpecVersion string `json:"spec_version"`
|
||||
Tags []any `json:"tags"`
|
||||
Extentions []byte `json:"extentions"`
|
||||
}
|
||||
|
||||
type Spec2Wrapper struct {
|
||||
@@ -43,3 +44,15 @@ type CharCard struct {
|
||||
Role string `json:"role"`
|
||||
FilePath string `json:"filepath"`
|
||||
}
|
||||
|
||||
func (cc *CharCard) ToSpec(userName string) *CharCardSpec {
|
||||
descr := strings.ReplaceAll(strings.ReplaceAll(cc.SysPrompt, cc.Role, "{{char}}"), userName, "{{user}}")
|
||||
return &CharCardSpec{
|
||||
Name: cc.Role,
|
||||
Description: descr,
|
||||
FirstMes: cc.FirstMsg,
|
||||
Spec: "chara_card_v2",
|
||||
SpecVersion: "2.0",
|
||||
Extentions: []byte("{}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,15 +8,22 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
embType = "tEXt"
|
||||
embType = "tEXt"
|
||||
cKey = "chara"
|
||||
IEND = "IEND"
|
||||
header = "\x89PNG\r\n\x1a\n"
|
||||
writeHeader = "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"
|
||||
)
|
||||
|
||||
var tEXtChunkDataSpecification = "%s\x00%s"
|
||||
|
||||
type PngEmbed struct {
|
||||
Key string
|
||||
Value string
|
||||
@@ -107,7 +114,7 @@ func readCardJson(fname string) (*models.CharCard, error) {
|
||||
return &card, nil
|
||||
}
|
||||
|
||||
func ReadDirCards(dirname, uname string) ([]*models.CharCard, error) {
|
||||
func ReadDirCards(dirname, uname string, log *slog.Logger) ([]*models.CharCard, error) {
|
||||
files, err := os.ReadDir(dirname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -121,7 +128,7 @@ func ReadDirCards(dirname, uname string) ([]*models.CharCard, error) {
|
||||
fpath := path.Join(dirname, f.Name())
|
||||
cc, err := ReadCard(fpath, uname)
|
||||
if err != nil {
|
||||
// logger.Warn("failed to load card", "error", err)
|
||||
log.Warn("failed to load card", "error", err)
|
||||
continue
|
||||
// return nil, err // better to log and continue
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@ var (
|
||||
ErrBadLength = errors.New("bad length")
|
||||
)
|
||||
|
||||
const header = "\x89PNG\r\n\x1a\n"
|
||||
|
||||
type PngChunk struct {
|
||||
typ string
|
||||
length int32
|
||||
|
||||
116
pngmeta/partswriter.go
Normal file
116
pngmeta/partswriter.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package pngmeta
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"elefant/models"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Writer struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func NewPNGWriter(w io.Writer) (*Writer, error) {
|
||||
if _, err := io.WriteString(w, writeHeader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Writer{w}, nil
|
||||
}
|
||||
|
||||
func (w *Writer) WriteChunk(length int32, typ string, r io.Reader) error {
|
||||
if err := binary.Write(w.w, binary.BigEndian, length); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.w.Write([]byte(typ)); err != nil {
|
||||
return err
|
||||
}
|
||||
checksummer := crc32.NewIEEE()
|
||||
checksummer.Write([]byte(typ))
|
||||
if _, err := io.CopyN(io.MultiWriter(w.w, checksummer), r, int64(length)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := binary.Write(w.w, binary.BigEndian, checksummer.Sum32()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteToPng(c *models.CharCardSpec, fpath, outfile string) error {
|
||||
data, err := os.ReadFile(fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
jsonData, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Base64 encode the JSON data
|
||||
base64Data := base64.StdEncoding.EncodeToString(jsonData)
|
||||
pe := PngEmbed{
|
||||
Key: cKey,
|
||||
Value: base64Data,
|
||||
}
|
||||
w, err := WritetEXtToPngBytes(data, pe)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(outfile, w.Bytes(), 0666)
|
||||
}
|
||||
|
||||
func WritetEXtToPngBytes(inputBytes []byte, pe PngEmbed) (outputBytes bytes.Buffer, err error) {
|
||||
if !(string(inputBytes[:8]) == header) {
|
||||
return outputBytes, errors.New("wrong file format")
|
||||
}
|
||||
reader := bytes.NewReader(inputBytes)
|
||||
pngr, err := NewPNGStepReader(reader)
|
||||
if err != nil {
|
||||
return outputBytes, fmt.Errorf("NewReader(): %s", err)
|
||||
}
|
||||
pngw, err := NewPNGWriter(&outputBytes)
|
||||
if err != nil {
|
||||
return outputBytes, fmt.Errorf("NewWriter(): %s", err)
|
||||
}
|
||||
for {
|
||||
chunk, err := pngr.Next()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
return outputBytes, fmt.Errorf("NextChunk(): %s", err)
|
||||
}
|
||||
if chunk.Type() != embType {
|
||||
// IENDChunkType will only appear on the final iteration of a valid PNG
|
||||
if chunk.Type() == IEND {
|
||||
// This is where we inject tEXtChunkType as the penultimate chunk with the new value
|
||||
newtEXtChunk := []byte(fmt.Sprintf(tEXtChunkDataSpecification, pe.Key, pe.Value))
|
||||
if err := pngw.WriteChunk(int32(len(newtEXtChunk)), embType, bytes.NewBuffer(newtEXtChunk)); err != nil {
|
||||
return outputBytes, fmt.Errorf("WriteChunk(): %s", err)
|
||||
}
|
||||
// Now we end the buffer with IENDChunkType chunk
|
||||
if err := pngw.WriteChunk(chunk.length, chunk.Type(), chunk); err != nil {
|
||||
return outputBytes, fmt.Errorf("WriteChunk(): %s", err)
|
||||
}
|
||||
} else {
|
||||
// writes back original chunk to buffer
|
||||
if err := pngw.WriteChunk(chunk.length, chunk.Type(), chunk); err != nil {
|
||||
return outputBytes, fmt.Errorf("WriteChunk(): %s", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if _, err := io.Copy(io.Discard, chunk); err != nil {
|
||||
return outputBytes, fmt.Errorf("io.Copy(io.Discard, chunk): %s", err)
|
||||
}
|
||||
}
|
||||
if err := chunk.Close(); err != nil {
|
||||
return outputBytes, fmt.Errorf("chunk.Close(): %s", err)
|
||||
}
|
||||
}
|
||||
return outputBytes, nil
|
||||
}
|
||||
23
tables.go
23
tables.go
@@ -4,9 +4,11 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"elefant/models"
|
||||
"elefant/pngmeta"
|
||||
"elefant/rag"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
@@ -14,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
func makeChatTable(chatMap map[string]models.Chat) *tview.Table {
|
||||
actions := []string{"load", "rename", "delete"}
|
||||
actions := []string{"load", "rename", "delete", "update card"}
|
||||
chatList := make([]string, len(chatMap))
|
||||
i := 0
|
||||
for name := range chatMap {
|
||||
@@ -62,6 +64,7 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table {
|
||||
tc.SetTextColor(tcell.ColorRed)
|
||||
chatActTable.SetSelectable(false, false)
|
||||
selectedChat := chatList[row]
|
||||
defer pages.RemovePage(historyPage)
|
||||
// notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text)
|
||||
switch tc.Text {
|
||||
case "load":
|
||||
@@ -98,8 +101,24 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table {
|
||||
textView.SetText(chatToText(cfg.ShowSys))
|
||||
pages.RemovePage(historyPage)
|
||||
return
|
||||
case "update card":
|
||||
// save updated card
|
||||
fi := strings.Index(selectedChat, "_")
|
||||
agentName := selectedChat[fi+1:]
|
||||
cc, ok := sysMap[agentName]
|
||||
if !ok {
|
||||
logger.Warn("no such card", "agent", agentName)
|
||||
//no:lint
|
||||
notifyUser("error", "no such card: "+agentName)
|
||||
}
|
||||
if err := pngmeta.WriteToPng(cc.ToSpec(cfg.UserRole), cc.FilePath, cc.FilePath); err != nil {
|
||||
logger.Error("failed to write charcard",
|
||||
"error", err)
|
||||
}
|
||||
// pages.RemovePage(historyPage)
|
||||
return
|
||||
default:
|
||||
pages.RemovePage(historyPage)
|
||||
// pages.RemovePage(historyPage)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
1
tools.go
1
tools.go
@@ -12,6 +12,7 @@ var (
|
||||
toolCallRE = regexp.MustCompile(`__tool_call__\s*([\s\S]*?)__tool_call__`)
|
||||
quotesRE = regexp.MustCompile(`(".*?")`)
|
||||
starRE = regexp.MustCompile(`(\*.*?\*)`)
|
||||
thinkRE = regexp.MustCompile(`(<think>.*?</think>)`)
|
||||
// codeBlokRE = regexp.MustCompile(`(\x60\x60\x60.*?\x60\x60\x60)`)
|
||||
basicSysMsg = `Large Language Model that helps user with any of his requests.`
|
||||
toolSysMsg = `You're a helpful assistant.
|
||||
|
||||
24
tui.go
24
tui.go
@@ -77,11 +77,13 @@ Press Enter to go back
|
||||
|
||||
func colorText() {
|
||||
// INFO: is there a better way to markdown?
|
||||
tv := textView.GetText(false)
|
||||
cq := quotesRE.ReplaceAllString(tv, `[orange:-:-]$1[-:-:-]`)
|
||||
text := textView.GetText(false)
|
||||
text = quotesRE.ReplaceAllString(text, `[orange:-:-]$1[-:-:-]`)
|
||||
text = starRE.ReplaceAllString(text, `[turquoise::i]$1[-:-:-]`)
|
||||
text = thinkRE.ReplaceAllString(text, `[turquoise::i]$1[-:-:-]`)
|
||||
// cb := codeBlockColor(cq)
|
||||
// cb := codeBlockRE.ReplaceAllString(cq, `[blue:black:i]$1[-:-:-]`)
|
||||
textView.SetText(starRE.ReplaceAllString(cq, `[turquoise::i]$1[-:-:-]`))
|
||||
textView.SetText(text)
|
||||
}
|
||||
|
||||
func updateStatusLine() {
|
||||
@@ -91,7 +93,7 @@ func updateStatusLine() {
|
||||
func initSysCards() ([]string, error) {
|
||||
labels := []string{}
|
||||
labels = append(labels, sysLabels...)
|
||||
cards, err := pngmeta.ReadDirCards(cfg.SysDir, cfg.UserRole)
|
||||
cards, err := pngmeta.ReadDirCards(cfg.SysDir, cfg.UserRole, logger)
|
||||
if err != nil {
|
||||
logger.Error("failed to read sys dir", "error", err)
|
||||
return nil, err
|
||||
@@ -116,7 +118,7 @@ func startNewChat() {
|
||||
logger.Warn("no such sys msg", "name", cfg.AssistantRole)
|
||||
}
|
||||
// set chat body
|
||||
chatBody.Messages = defaultStarter
|
||||
chatBody.Messages = chatBody.Messages[:2]
|
||||
textView.SetText(chatToText(cfg.ShowSys))
|
||||
newChat := &models.Chat{
|
||||
ID: id + 1,
|
||||
@@ -186,7 +188,17 @@ func init() {
|
||||
SetChangedFunc(func() {
|
||||
app.Draw()
|
||||
})
|
||||
textView.SetBorder(true).SetTitle("chat")
|
||||
// textView.SetBorder(true).SetTitle("chat")
|
||||
textView.SetDoneFunc(func(key tcell.Key) {
|
||||
currentSelection := textView.GetHighlights()
|
||||
if key == tcell.KeyEnter {
|
||||
if len(currentSelection) > 0 {
|
||||
textView.Highlight()
|
||||
} else {
|
||||
textView.Highlight("0").ScrollToHighlight()
|
||||
}
|
||||
}
|
||||
})
|
||||
focusSwitcher[textArea] = textView
|
||||
focusSwitcher[textView] = textArea
|
||||
position = tview.NewTextView().
|
||||
|
||||
Reference in New Issue
Block a user