Enha: db chat management
This commit is contained in:
@@ -13,7 +13,7 @@
|
|||||||
- tab to switch selection between textview and textarea (input and chat); +
|
- tab to switch selection between textview and textarea (input and chat); +
|
||||||
- basic tools: memorize and recall;
|
- basic tools: memorize and recall;
|
||||||
- stop stream from the bot; +
|
- stop stream from the bot; +
|
||||||
- sqlitedb instead of chatfiles;
|
- sqlitedb instead of chatfiles; +
|
||||||
- sqlite for the bot memory;
|
- sqlite for the bot memory;
|
||||||
|
|
||||||
### FIX:
|
### FIX:
|
||||||
|
|||||||
93
bot.go
93
bot.go
@@ -11,7 +11,6 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -34,7 +33,7 @@ var (
|
|||||||
historyDir = "./history/"
|
historyDir = "./history/"
|
||||||
// TODO: pass as an cli arg
|
// TODO: pass as an cli arg
|
||||||
showSystemMsgs bool
|
showSystemMsgs bool
|
||||||
chatFileLoaded string
|
activeChatName string
|
||||||
chunkChan = make(chan string, 10)
|
chunkChan = make(chan string, 10)
|
||||||
streamDone = make(chan bool, 1)
|
streamDone = make(chan bool, 1)
|
||||||
chatBody *models.ChatBody
|
chatBody *models.ChatBody
|
||||||
@@ -89,14 +88,12 @@ var fnMap = map[string]fnSig{
|
|||||||
// ====
|
// ====
|
||||||
|
|
||||||
func getUserInput(userPrompt string) string {
|
func getUserInput(userPrompt string) string {
|
||||||
// fmt.Printf("<🤖>: %s\n<user>:", botMsg)
|
|
||||||
fmt.Printf(userPrompt)
|
fmt.Printf(userPrompt)
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
line, err := reader.ReadString('\n')
|
line, err := reader.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err) // think about it
|
panic(err) // think about it
|
||||||
}
|
}
|
||||||
// fmt.Printf("read line: %s-\n", line)
|
|
||||||
return line
|
return line
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +149,7 @@ func sendMsgToLLM(body io.Reader) (any, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
llmResp = append(llmResp, llmchunk)
|
llmResp = append(llmResp, llmchunk)
|
||||||
logger.Info("streamview", "chunk", llmchunk)
|
// logger.Info("streamview", "chunk", llmchunk)
|
||||||
// if llmchunk.Choices[len(llmchunk.Choices)-1].FinishReason != "chat.completion.chunk" {
|
// if llmchunk.Choices[len(llmchunk.Choices)-1].FinishReason != "chat.completion.chunk" {
|
||||||
if llmchunk.Choices[len(llmchunk.Choices)-1].FinishReason == "stop" {
|
if llmchunk.Choices[len(llmchunk.Choices)-1].FinishReason == "stop" {
|
||||||
streamDone <- true
|
streamDone <- true
|
||||||
@@ -192,18 +189,12 @@ out:
|
|||||||
})
|
})
|
||||||
// bot msg is done;
|
// bot msg is done;
|
||||||
// now check it for func call
|
// now check it for func call
|
||||||
logChat(chatFileLoaded, chatBody.Messages)
|
// logChat(activeChatName, chatBody.Messages)
|
||||||
findCall(respText.String(), tv)
|
err := updateStorageChat(activeChatName, chatBody.Messages)
|
||||||
}
|
|
||||||
|
|
||||||
func logChat(fname string, msgs []models.MessagesStory) {
|
|
||||||
data, err := json.MarshalIndent(msgs, "", " ")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("failed to marshal", "error", err)
|
logger.Warn("failed to update storage", "error", err, "name", activeChatName)
|
||||||
}
|
|
||||||
if err := os.WriteFile(fname, data, 0666); err != nil {
|
|
||||||
logger.Error("failed to write log", "error", err)
|
|
||||||
}
|
}
|
||||||
|
findCall(respText.String(), tv)
|
||||||
}
|
}
|
||||||
|
|
||||||
func findCall(msg string, tv *tview.TextView) {
|
func findCall(msg string, tv *tview.TextView) {
|
||||||
@@ -235,74 +226,6 @@ func findCall(msg string, tv *tview.TextView) {
|
|||||||
// return func result to the llm
|
// return func result to the llm
|
||||||
}
|
}
|
||||||
|
|
||||||
func listHistoryFiles(dir string) ([]string, error) {
|
|
||||||
files, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("failed to readdir", "error", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp := make([]string, len(files))
|
|
||||||
for i, f := range files {
|
|
||||||
resp[i] = path.Join(dir, f.Name())
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func findLatestChat(dir string) string {
|
|
||||||
files, err := listHistoryFiles(dir)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
latestF string
|
|
||||||
newestTime int64
|
|
||||||
)
|
|
||||||
logger.Info("filelist", "list", files)
|
|
||||||
for _, fn := range files {
|
|
||||||
fi, err := os.Stat(fn)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("failed to get stat", "error", err, "name", fn)
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
currTime := fi.ModTime().Unix()
|
|
||||||
if currTime > newestTime {
|
|
||||||
newestTime = currTime
|
|
||||||
latestF = fn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return latestF
|
|
||||||
}
|
|
||||||
|
|
||||||
func readHistoryChat(fn string) ([]models.MessagesStory, error) {
|
|
||||||
content, err := os.ReadFile(fn)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("failed to read file", "error", err, "name", fn)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp := []models.MessagesStory{}
|
|
||||||
if err := json.Unmarshal(content, &resp); err != nil {
|
|
||||||
logger.Error("failed to unmarshal", "error", err, "name", fn)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
chatFileLoaded = fn
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadOldChatOrGetNew(fns ...string) []models.MessagesStory {
|
|
||||||
// find last chat
|
|
||||||
fn := findLatestChat(historyDir)
|
|
||||||
if len(fns) > 0 {
|
|
||||||
fn = fns[0]
|
|
||||||
}
|
|
||||||
logger.Info("reading history from file", "filename", fn)
|
|
||||||
history, err := readHistoryChat(fn)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn("faield to load history chat", "error", err)
|
|
||||||
return defaultStarter
|
|
||||||
}
|
|
||||||
return history
|
|
||||||
}
|
|
||||||
|
|
||||||
func chatToTextSlice(showSys bool) []string {
|
func chatToTextSlice(showSys bool) []string {
|
||||||
resp := make([]string, len(chatBody.Messages))
|
resp := make([]string, len(chatBody.Messages))
|
||||||
for i, msg := range chatBody.Messages {
|
for i, msg := range chatBody.Messages {
|
||||||
@@ -353,10 +276,13 @@ func init() {
|
|||||||
if err := os.MkdirAll(historyDir, os.ModePerm); err != nil {
|
if err := os.MkdirAll(historyDir, os.ModePerm); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
store = storage.NewProviderSQL("test.db")
|
||||||
// defer file.Close()
|
// defer file.Close()
|
||||||
logger = slog.New(slog.NewTextHandler(file, nil))
|
logger = slog.New(slog.NewTextHandler(file, nil))
|
||||||
logger.Info("test msg")
|
logger.Info("test msg")
|
||||||
// https://github.com/coreydaley/ggerganov-llama.cpp/blob/master/examples/server/README.md
|
// https://github.com/coreydaley/ggerganov-llama.cpp/blob/master/examples/server/README.md
|
||||||
|
// load all chats in memory
|
||||||
|
loadHistoryChats()
|
||||||
lastChat := loadOldChatOrGetNew()
|
lastChat := loadOldChatOrGetNew()
|
||||||
logger.Info("loaded history", "chat", lastChat)
|
logger.Info("loaded history", "chat", lastChat)
|
||||||
chatBody = &models.ChatBody{
|
chatBody = &models.ChatBody{
|
||||||
@@ -364,5 +290,4 @@ func init() {
|
|||||||
Stream: true,
|
Stream: true,
|
||||||
Messages: lastChat,
|
Messages: lastChat,
|
||||||
}
|
}
|
||||||
store = storage.NewProviderSQL("test.db")
|
|
||||||
}
|
}
|
||||||
|
|||||||
16
main.go
16
main.go
@@ -60,7 +60,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
chatOpts := []string{"cancel", "new"}
|
chatOpts := []string{"cancel", "new"}
|
||||||
fList, err := listHistoryFiles(historyDir)
|
fList, err := loadHistoryChats()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -74,18 +74,16 @@ func main() {
|
|||||||
// set chat body
|
// set chat body
|
||||||
chatBody.Messages = defaultStarter
|
chatBody.Messages = defaultStarter
|
||||||
textView.SetText(chatToText(showSystemMsgs))
|
textView.SetText(chatToText(showSystemMsgs))
|
||||||
chatFileLoaded = path.Join(historyDir, fmt.Sprintf("%d_chat.json", time.Now().Unix()))
|
activeChatName = path.Join(historyDir, fmt.Sprintf("%d_chat.json", time.Now().Unix()))
|
||||||
pages.RemovePage("history")
|
pages.RemovePage("history")
|
||||||
return
|
return
|
||||||
// set text
|
// set text
|
||||||
case "cancel":
|
case "cancel":
|
||||||
pages.RemovePage("history")
|
pages.RemovePage("history")
|
||||||
// pages.ShowPage("main")
|
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
// fn := path.Join(historyDir, buttonLabel)
|
|
||||||
fn := buttonLabel
|
fn := buttonLabel
|
||||||
history, err := readHistoryChat(fn)
|
history, err := loadHistoryChat(fn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("failed to read history file", "filename", fn)
|
logger.Error("failed to read history file", "filename", fn)
|
||||||
pages.RemovePage("history")
|
pages.RemovePage("history")
|
||||||
@@ -93,7 +91,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
chatBody.Messages = history
|
chatBody.Messages = history
|
||||||
textView.SetText(chatToText(showSystemMsgs))
|
textView.SetText(chatToText(showSystemMsgs))
|
||||||
chatFileLoaded = fn
|
activeChatName = fn
|
||||||
pages.RemovePage("history")
|
pages.RemovePage("history")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -104,14 +102,11 @@ func main() {
|
|||||||
editArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
editArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
if event.Key() == tcell.KeyEscape && editMode {
|
if event.Key() == tcell.KeyEscape && editMode {
|
||||||
editedMsg := editArea.GetText()
|
editedMsg := editArea.GetText()
|
||||||
// TODO: trim msg number and icon
|
|
||||||
chatBody.Messages[selectedIndex].Content = editedMsg
|
chatBody.Messages[selectedIndex].Content = editedMsg
|
||||||
// change textarea
|
// change textarea
|
||||||
textView.SetText(chatToText(showSystemMsgs))
|
textView.SetText(chatToText(showSystemMsgs))
|
||||||
pages.RemovePage("editArea")
|
pages.RemovePage("editArea")
|
||||||
editMode = false
|
editMode = false
|
||||||
// panic("do we get here?")
|
|
||||||
// pages.ShowPage("main")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return event
|
return event
|
||||||
@@ -151,7 +146,8 @@ func main() {
|
|||||||
textView.ScrollToEnd()
|
textView.ScrollToEnd()
|
||||||
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
if event.Key() == tcell.KeyF1 {
|
if event.Key() == tcell.KeyF1 {
|
||||||
fList, err := listHistoryFiles(historyDir)
|
// fList, err := listHistoryFiles(historyDir)
|
||||||
|
fList, err := loadHistoryChats()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
13
models/db.go
13
models/db.go
@@ -1,6 +1,9 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type Chat struct {
|
type Chat struct {
|
||||||
ID uint32 `db:"id" json:"id"`
|
ID uint32 `db:"id" json:"id"`
|
||||||
@@ -9,3 +12,11 @@ type Chat struct {
|
|||||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c Chat) ToHistory() ([]MessagesStory, error) {
|
||||||
|
resp := []MessagesStory{}
|
||||||
|
if err := json.Unmarshal([]byte(c.Msgs), &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|||||||
92
session.go
Normal file
92
session.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"elefant/models"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
chatMap = make(map[string]*models.Chat)
|
||||||
|
)
|
||||||
|
|
||||||
|
func historyToSJSON(msgs []models.MessagesStory) (string, error) {
|
||||||
|
data, err := json.Marshal(msgs)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if data == nil {
|
||||||
|
return "", fmt.Errorf("nil data")
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateStorageChat(name string, msgs []models.MessagesStory) error {
|
||||||
|
var err error
|
||||||
|
chat, ok := chatMap[name]
|
||||||
|
if !ok {
|
||||||
|
err = fmt.Errorf("failed to find active chat; map:%v; key:%s", chatMap, name)
|
||||||
|
logger.Error("failed to find active chat", "map", chatMap, "key", name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
chat.Msgs, err = historyToSJSON(msgs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
chat.UpdatedAt = time.Now()
|
||||||
|
_, err = store.UpsertChat(chat)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadHistoryChats() ([]string, error) {
|
||||||
|
chats, err := store.ListChats()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp := []string{}
|
||||||
|
for _, chat := range chats {
|
||||||
|
if chat.Name == "" {
|
||||||
|
chat.Name = fmt.Sprintf("%d_%v", chat.ID, chat.CreatedAt.Unix())
|
||||||
|
}
|
||||||
|
resp = append(resp, chat.Name)
|
||||||
|
chatMap[chat.Name] = &chat
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadHistoryChat(chatName string) ([]models.MessagesStory, error) {
|
||||||
|
chat, ok := chatMap[chatName]
|
||||||
|
if !ok {
|
||||||
|
err := fmt.Errorf("failed to read chat")
|
||||||
|
logger.Error("failed to read chat", "name", chatName)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
activeChatName = chatName
|
||||||
|
return chat.ToHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadOldChatOrGetNew() []models.MessagesStory {
|
||||||
|
// find last chat
|
||||||
|
chat, err := store.GetLastChat()
|
||||||
|
newChat := &models.Chat{
|
||||||
|
ID: 0,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
newChat.Name = fmt.Sprintf("%d_%v", chat.ID, chat.CreatedAt.Unix())
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("failed to load history chat", "error", err)
|
||||||
|
activeChatName = newChat.Name
|
||||||
|
chatMap[newChat.Name] = newChat
|
||||||
|
return defaultStarter
|
||||||
|
}
|
||||||
|
history, err := chat.ToHistory()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("failed to load history chat", "error", err)
|
||||||
|
activeChatName = newChat.Name
|
||||||
|
chatMap[newChat.Name] = newChat
|
||||||
|
return defaultStarter
|
||||||
|
}
|
||||||
|
return history
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
type ChatHistory interface {
|
type ChatHistory interface {
|
||||||
ListChats() ([]models.Chat, error)
|
ListChats() ([]models.Chat, error)
|
||||||
GetChatByID(id uint32) (*models.Chat, error)
|
GetChatByID(id uint32) (*models.Chat, error)
|
||||||
|
GetLastChat() (*models.Chat, error)
|
||||||
UpsertChat(chat *models.Chat) (*models.Chat, error)
|
UpsertChat(chat *models.Chat) (*models.Chat, error)
|
||||||
RemoveChat(id uint32) error
|
RemoveChat(id uint32) error
|
||||||
}
|
}
|
||||||
@@ -31,6 +32,12 @@ func (p ProviderSQL) GetChatByID(id uint32) (*models.Chat, error) {
|
|||||||
return &resp, err
|
return &resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p ProviderSQL) GetLastChat() (*models.Chat, error) {
|
||||||
|
resp := models.Chat{}
|
||||||
|
err := p.db.Get(&resp, "SELECT * FROM chat ORDER BY updated_at DESC LIMIT 1")
|
||||||
|
return &resp, err
|
||||||
|
}
|
||||||
|
|
||||||
func (p ProviderSQL) UpsertChat(chat *models.Chat) (*models.Chat, error) {
|
func (p ProviderSQL) UpsertChat(chat *models.Chat) (*models.Chat, error) {
|
||||||
// Prepare the SQL statement
|
// Prepare the SQL statement
|
||||||
query := `
|
query := `
|
||||||
|
|||||||
Reference in New Issue
Block a user