Merge branch 'master' into doc/tutorial

This commit is contained in:
Grail Finder
2025-12-14 10:04:48 +03:00
4 changed files with 166 additions and 96 deletions

41
bot.go
View File

@@ -826,6 +826,30 @@ func charToStart(agentName string) bool {
return true
}
func updateModelLists() {
var err error
if cfg.OpenRouterToken != "" {
ORFreeModels, err = fetchORModels(true)
if err != nil {
logger.Warn("failed to fetch or models", "error", err)
}
}
// if llama.cpp started after gf-lt?
LocalModels, err = fetchLCPModels()
if err != nil {
logger.Warn("failed to fetch llama.cpp models", "error", err)
}
}
func updateModelListsTicker() {
updateModelLists() // run on the start
ticker := time.NewTicker(time.Minute * 1)
for {
<-ticker.C
updateModelLists()
}
}
func init() {
var err error
cfg, err = config.LoadConfig("config.toml")
@@ -878,22 +902,6 @@ func init() {
playerOrder = []string{cfg.UserRole, cfg.AssistantRole, cfg.CluedoRole2}
cluedoState = extra.CluedoPrepCards(playerOrder)
}
if cfg.OpenRouterToken != "" {
go func() {
ORModels, err := fetchORModels(true)
if err != nil {
logger.Error("failed to fetch or models", "error", err)
} else {
ORFreeModels = ORModels
}
}()
}
go func() {
LocalModels, err = fetchLCPModels()
if err != nil {
logger.Error("failed to fetch llama.cpp models", "error", err)
}
}()
choseChunkParser()
httpClient = createClient(time.Second * 15)
if cfg.TTS_ENABLED {
@@ -902,4 +910,5 @@ func init() {
if cfg.STT_ENABLED {
asr = extra.NewSTT(logger, cfg)
}
go updateModelListsTicker()
}

View File

@@ -1,6 +1,8 @@
package config
import (
"os"
"github.com/BurntSushi/toml"
)
@@ -84,6 +86,13 @@ func LoadConfig(fn string) (*Config, error) {
config.OpenRouterCompletionAPI: config.OpenRouterChatAPI,
config.OpenRouterChatAPI: config.ChatAPI,
}
// check env if keys not in config
if config.OpenRouterToken == "" {
config.OpenRouterToken = os.Getenv("OPENROUTER_API_KEY")
}
if config.DeepSeekToken == "" {
config.DeepSeekToken = os.Getenv("DEEPSEEK_API_KEY")
}
// Build ApiLinks slice with only non-empty API links
// Only include DeepSeek APIs if DeepSeekToken is provided
if config.DeepSeekToken != "" {
@@ -94,7 +103,6 @@ func LoadConfig(fn string) (*Config, error) {
config.ApiLinks = append(config.ApiLinks, config.DeepSeekCompletionAPI)
}
}
// Only include OpenRouter APIs if OpenRouterToken is provided
if config.OpenRouterToken != "" {
if config.OpenRouterChatAPI != "" {
@@ -104,7 +112,6 @@ func LoadConfig(fn string) (*Config, error) {
config.ApiLinks = append(config.ApiLinks, config.OpenRouterCompletionAPI)
}
}
// Always include basic APIs
if config.ChatAPI != "" {
config.ApiLinks = append(config.ApiLinks, config.ChatAPI)

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"slices"
"strconv"
"strings"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
@@ -134,9 +135,16 @@ func makePropsTable(props map[string]float32) *tview.Table {
addListPopupRow("Select an api", apiLinks, cfg.CurrentAPI, func(option string) {
cfg.CurrentAPI = option
})
var modelList []string
// INFO: modelList is chosen based on current api link
if strings.Contains(cfg.CurrentAPI, "api.deepseek.com/") {
modelList = []string{chatBody.Model, "deepseek-chat", "deepseek-reasoner"}
} else if strings.Contains(cfg.CurrentAPI, "opentouter.ai") {
modelList = ORFreeModels
} else { // would match on localhost but what if llama.cpp served non localy?
modelList = LocalModels
}
// Prepare model list dropdown
modelList := []string{chatBody.Model, "deepseek-chat", "deepseek-reasoner"}
modelList = append(modelList, ORFreeModels...)
addListPopupRow("Select a model", modelList, chatBody.Model, func(option string) {
chatBody.Model = option
})

168
tables.go
View File

@@ -33,11 +33,13 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table {
case 0:
chatActTable.SetCell(r, c,
tview.NewTableCell(chatList[r]).
SetSelectable(false).
SetTextColor(color).
SetAlign(tview.AlignCenter))
case 1:
chatActTable.SetCell(r, c,
tview.NewTableCell(chatMap[chatList[r]].Msgs[len(chatMap[chatList[r]].Msgs)-30:]).
SetSelectable(false).
SetTextColor(color).
SetAlign(tview.AlignCenter))
default:
@@ -48,14 +50,11 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table {
}
}
}
chatActTable.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) {
chatActTable.Select(0, 0).SetSelectable(true, true).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') {
pages.RemovePage(historyPage)
return
}
if key == tcell.KeyEnter {
chatActTable.SetSelectable(true, true)
}
}).SetSelectedFunc(func(row int, column int) {
tc := chatActTable.GetCell(row, column)
tc.SetTextColor(tcell.ColorRed)
@@ -192,21 +191,21 @@ func makeRAGTable(fileList []string) *tview.Flex {
ragflex := tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(longStatusView, 0, 10, false).
AddItem(fileTable, 0, 60, true)
// Add the exit option as the first row (row 0)
fileTable.SetCell(0, 0,
tview.NewTableCell("Exit RAG manager").
SetTextColor(tcell.ColorWhite).
SetAlign(tview.AlignCenter))
SetAlign(tview.AlignCenter).
SetSelectable(false))
fileTable.SetCell(0, 1,
tview.NewTableCell("(Close without action)").
SetTextColor(tcell.ColorGray).
SetAlign(tview.AlignCenter))
SetAlign(tview.AlignCenter).
SetSelectable(false))
fileTable.SetCell(0, 2,
tview.NewTableCell("exit").
SetTextColor(tcell.ColorGray).
SetAlign(tview.AlignCenter))
// Add the file rows starting from row 1
for r := 0; r < rows; r++ {
for c := 0; c < cols; c++ {
@@ -215,8 +214,15 @@ func makeRAGTable(fileList []string) *tview.Flex {
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0
tview.NewTableCell(fileList[r]).
SetTextColor(color).
SetAlign(tview.AlignCenter))
} else {
SetAlign(tview.AlignCenter).
SetSelectable(false))
} else if c == 1 { // Action description column - not selectable
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0
tview.NewTableCell("(Action)").
SetTextColor(color).
SetAlign(tview.AlignCenter).
SetSelectable(false))
} else { // Action button column - selectable
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0
tview.NewTableCell(actions[c-1]).
SetTextColor(color).
@@ -250,29 +256,32 @@ func makeRAGTable(fileList []string) *tview.Flex {
}
}
}()
fileTable.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) {
fileTable.Select(0, 0).
SetFixed(1, 1).
SetSelectable(true, false).
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') || key == tcell.KeyCtrlX {
pages.RemovePage(RAGPage)
return
}
if key == tcell.KeyEnter {
fileTable.SetSelectable(true, true)
}
}).SetSelectedFunc(func(row int, column int) {
// If user selects a non-actionable column (0 or 1), move to first action column (2)
if column <= 1 {
if fileTable.GetColumnCount() > 2 {
fileTable.Select(row, 2) // Select first action column
}
return
}
// defer pages.RemovePage(RAGPage)
tc := fileTable.GetCell(row, column)
tc.SetTextColor(tcell.ColorRed)
fileTable.SetSelectable(false, false)
// Check if the selected row is the exit row (row 0) - do this first to avoid index issues
if row == 0 {
pages.RemovePage(RAGPage)
return
}
// For file rows, get the filename (row index - 1 because of the exit row at index 0)
fpath := fileList[row-1] // -1 to account for the exit row at index 0
// notification := fmt.Sprintf("chat: %s; action: %s", fpath, tc.Text)
switch tc.Text {
case "load":
@@ -303,7 +312,6 @@ func makeRAGTable(fileList []string) *tview.Flex {
return
}
})
// Add input capture to the flex container to handle 'x' key for closing
ragflex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
@@ -312,7 +320,6 @@ func makeRAGTable(fileList []string) *tview.Flex {
}
return event
})
return ragflex
}
@@ -331,21 +338,21 @@ func makeLoadedRAGTable(fileList []string) *tview.Flex {
ragflex := tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(longStatusView, 0, 10, false).
AddItem(fileTable, 0, 60, true)
// Add the exit option as the first row (row 0)
fileTable.SetCell(0, 0,
tview.NewTableCell("Exit Loaded Files manager").
SetTextColor(tcell.ColorWhite).
SetAlign(tview.AlignCenter))
SetAlign(tview.AlignCenter).
SetSelectable(false))
fileTable.SetCell(0, 1,
tview.NewTableCell("(Close without action)").
SetTextColor(tcell.ColorGray).
SetAlign(tview.AlignCenter))
SetAlign(tview.AlignCenter).
SetSelectable(false))
fileTable.SetCell(0, 2,
tview.NewTableCell("exit").
SetTextColor(tcell.ColorGray).
SetAlign(tview.AlignCenter))
// Add the file rows starting from row 1
for r := 0; r < rows; r++ {
for c := 0; c < cols; c++ {
@@ -354,8 +361,15 @@ func makeLoadedRAGTable(fileList []string) *tview.Flex {
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0
tview.NewTableCell(fileList[r]).
SetTextColor(color).
SetAlign(tview.AlignCenter))
} else {
SetAlign(tview.AlignCenter).
SetSelectable(false))
} else if c == 1 { // Action description column - not selectable
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0
tview.NewTableCell("(Action)").
SetTextColor(color).
SetAlign(tview.AlignCenter).
SetSelectable(false))
} else { // Action button column - selectable
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0
tview.NewTableCell(actions[c-1]).
SetTextColor(color).
@@ -363,29 +377,33 @@ func makeLoadedRAGTable(fileList []string) *tview.Flex {
}
}
}
fileTable.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) {
fileTable.Select(0, 0).
SetFixed(1, 1).
SetSelectable(true, false).
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') || key == tcell.KeyCtrlX {
pages.RemovePage(RAGLoadedPage)
return
}
if key == tcell.KeyEnter {
fileTable.SetSelectable(true, true)
}
}).SetSelectedFunc(func(row int, column int) {
// If user selects a non-actionable column (0 or 1), move to first action column (2)
if column <= 1 {
if fileTable.GetColumnCount() > 2 {
fileTable.Select(row, 2) // Select first action column
}
return
}
tc := fileTable.GetCell(row, column)
tc.SetTextColor(tcell.ColorRed)
fileTable.SetSelectable(false, false)
// Check if the selected row is the exit row (row 0) - do this first to avoid index issues
if row == 0 {
pages.RemovePage(RAGLoadedPage)
return
}
// For file rows, get the filename (row index - 1 because of the exit row at index 0)
fpath := fileList[row-1] // -1 to account for the exit row at index 0
switch tc.Text {
case "delete":
if err := ragger.RemoveFile(fpath); err != nil {
@@ -403,7 +421,6 @@ func makeLoadedRAGTable(fileList []string) *tview.Flex {
return
}
})
// Add input capture to the flex container to handle 'x' key for closing
ragflex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
@@ -412,7 +429,6 @@ func makeLoadedRAGTable(fileList []string) *tview.Flex {
}
return event
})
return ragflex
}
@@ -428,8 +444,9 @@ func makeAgentTable(agentList []string) *tview.Table {
chatActTable.SetCell(r, c,
tview.NewTableCell(agentList[r]).
SetTextColor(color).
SetAlign(tview.AlignCenter))
} else {
SetAlign(tview.AlignCenter).
SetSelectable(false))
} else if c == 1 {
if actions[c-1] == "filepath" {
cc, ok := sysMap[agentList[r]]
if !ok {
@@ -438,28 +455,41 @@ func makeAgentTable(agentList []string) *tview.Table {
chatActTable.SetCell(r, c,
tview.NewTableCell(cc.FilePath).
SetTextColor(color).
SetAlign(tview.AlignCenter))
SetAlign(tview.AlignCenter).
SetSelectable(false))
continue
}
chatActTable.SetCell(r, c,
tview.NewTableCell(actions[c-1]).
SetTextColor(color).
SetAlign(tview.AlignCenter))
} else {
chatActTable.SetCell(r, c,
tview.NewTableCell(actions[c-1]).
SetTextColor(color).
SetAlign(tview.AlignCenter))
}
}
}
chatActTable.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) {
chatActTable.Select(0, 0).
SetFixed(1, 1).
SetSelectable(true, false).
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') {
pages.RemovePage(agentPage)
return
}
if key == tcell.KeyEnter {
chatActTable.SetSelectable(true, true)
}
}).SetSelectedFunc(func(row int, column int) {
// If user selects a non-actionable column (0 or 1), move to first action column (2)
if column <= 1 {
if chatActTable.GetColumnCount() > 2 {
chatActTable.Select(row, 2) // Select first action column
}
return
}
tc := chatActTable.GetCell(row, column)
tc.SetTextColor(tcell.ColorRed)
chatActTable.SetSelectable(false, false)
selected := agentList[row]
// notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text)
switch tc.Text {
@@ -528,7 +558,8 @@ func makeCodeBlockTable(codeBlocks []string) *tview.Table {
table.SetCell(r, c,
tview.NewTableCell(codeBlocks[r][:previewLen]).
SetTextColor(color).
SetAlign(tview.AlignCenter))
SetAlign(tview.AlignCenter).
SetSelectable(false))
} else {
table.SetCell(r, c,
tview.NewTableCell(actions[c-1]).
@@ -537,18 +568,25 @@ func makeCodeBlockTable(codeBlocks []string) *tview.Table {
}
}
}
table.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) {
table.Select(0, 0).
SetFixed(1, 1).
SetSelectable(true, false).
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') {
pages.RemovePage(codeBlockPage)
return
}
if key == tcell.KeyEnter {
table.SetSelectable(true, true)
}
}).SetSelectedFunc(func(row int, column int) {
// If user selects a non-actionable column (0), move to first action column (1)
if column == 0 {
if table.GetColumnCount() > 1 {
table.Select(row, 1) // Select first action column
}
return
}
tc := table.GetCell(row, column)
tc.SetTextColor(tcell.ColorRed)
table.SetSelectable(false, false)
selected := codeBlocks[row]
// notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text)
switch tc.Text {
@@ -592,7 +630,8 @@ func makeImportChatTable(filenames []string) *tview.Table {
chatActTable.SetCell(r, c,
tview.NewTableCell(filenames[r]).
SetTextColor(color).
SetAlign(tview.AlignCenter))
SetAlign(tview.AlignCenter).
SetSelectable(false))
} else {
chatActTable.SetCell(r, c,
tview.NewTableCell(actions[c-1]).
@@ -601,18 +640,25 @@ func makeImportChatTable(filenames []string) *tview.Table {
}
}
}
chatActTable.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) {
chatActTable.Select(0, 0).
SetFixed(1, 1).
SetSelectable(true, false).
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') {
pages.RemovePage(historyPage)
return
}
if key == tcell.KeyEnter {
chatActTable.SetSelectable(true, true)
}
}).SetSelectedFunc(func(row int, column int) {
// If user selects a non-actionable column (0), move to first action column (1)
if column == 0 {
if chatActTable.GetColumnCount() > 1 {
chatActTable.Select(row, 1) // Select first action column
}
return
}
tc := chatActTable.GetCell(row, column)
tc.SetTextColor(tcell.ColorRed)
chatActTable.SetSelectable(false, false)
selected := filenames[row]
// notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text)
switch tc.Text {