Compare commits
7 Commits
feat/resp-
...
978369eeaa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
978369eeaa | ||
|
|
c39e1c267d | ||
|
|
9af21895c6 | ||
|
|
e3bd6f219f | ||
|
|
ae62c2c8d8 | ||
|
|
04db7c2f01 | ||
|
|
3d889e70b5 |
8
Makefile
8
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: setconfig run lint setup-whisper build-whisper download-whisper-model docker-up docker-down docker-logs noextra-run
|
||||
.PHONY: setconfig run lint setup-whisper build-whisper download-whisper-model docker-up docker-down docker-logs noextra-run installdelve checkdelve
|
||||
|
||||
run: setconfig
|
||||
go build -tags extra -o gf-lt && ./gf-lt
|
||||
@@ -15,6 +15,12 @@ noextra-run: setconfig
|
||||
setconfig:
|
||||
find config.toml &>/dev/null || cp config.example.toml config.toml
|
||||
|
||||
installdelve:
|
||||
go install github.com/go-delve/delve/cmd/dlv@latest
|
||||
|
||||
checkdelve:
|
||||
which dlv &>/dev/null || installdelve
|
||||
|
||||
lint: ## Run linters. Use make install-linters first.
|
||||
golangci-lint run -c .golangci.yml ./...
|
||||
|
||||
|
||||
43
bot.go
43
bot.go
@@ -411,14 +411,21 @@ func fetchLCPModelsWithLoadStatus() ([]string, error) {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]string, 0, len(models.Data))
|
||||
for _, m := range models.Data {
|
||||
li := 0 // loaded index
|
||||
for i, m := range models.Data {
|
||||
modelName := m.ID
|
||||
if m.Status.Value == "loaded" {
|
||||
modelName = "(loaded) " + modelName
|
||||
li = i
|
||||
}
|
||||
result = append(result, modelName)
|
||||
}
|
||||
return result, nil
|
||||
if li == 0 {
|
||||
return result, nil // no loaded models
|
||||
}
|
||||
loadedModel := result[li]
|
||||
result = append(result[:li], result[li+1:]...)
|
||||
return slices.Concat([]string{loadedModel}, result), nil
|
||||
}
|
||||
|
||||
// fetchLCPModelsWithStatus returns the full LCPModels struct including status information.
|
||||
@@ -569,7 +576,6 @@ func sendMsgToLLM(body io.Reader) {
|
||||
streamDone <- true
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the initial response is an error before starting to stream
|
||||
if resp.StatusCode >= 400 {
|
||||
// Read the response body to get detailed error information
|
||||
@@ -584,7 +590,6 @@ func sendMsgToLLM(body io.Reader) {
|
||||
streamDone <- true
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the error response for detailed information
|
||||
detailedError := extractDetailedErrorFromBytes(bodyBytes, resp.StatusCode)
|
||||
logger.Error("API returned error status", "status_code", resp.StatusCode, "detailed_error", detailedError)
|
||||
@@ -710,7 +715,6 @@ func sendMsgToLLM(body io.Reader) {
|
||||
tokenCount++
|
||||
}
|
||||
}
|
||||
|
||||
// When we get content and have been streaming reasoning, close the thinking block
|
||||
if chunk.Chunk != "" && hasReasoning && !reasoningSent {
|
||||
// Close the thinking block before sending actual content
|
||||
@@ -718,7 +722,6 @@ func sendMsgToLLM(body io.Reader) {
|
||||
tokenCount++
|
||||
reasoningSent = true
|
||||
}
|
||||
|
||||
// bot sends way too many \n
|
||||
answerText = strings.ReplaceAll(chunk.Chunk, "\n\n", "\n")
|
||||
// Accumulate text to check for stop strings that might span across chunks
|
||||
@@ -764,12 +767,10 @@ func chatRagUse(qText string) (string, error) {
|
||||
questions[i] = q.Text
|
||||
logger.Debug("RAG question extracted", "index", i, "question", q.Text)
|
||||
}
|
||||
|
||||
if len(questions) == 0 {
|
||||
logger.Warn("No questions extracted from query text", "query", qText)
|
||||
return "No related results from RAG vector storage.", nil
|
||||
}
|
||||
|
||||
respVecs := []models.VectorRow{}
|
||||
for i, q := range questions {
|
||||
logger.Debug("Processing RAG question", "index", i, "question", q)
|
||||
@@ -779,7 +780,6 @@ func chatRagUse(qText string) (string, error) {
|
||||
continue
|
||||
}
|
||||
logger.Debug("Got embeddings for question", "index", i, "question_len", len(q), "embedding_len", len(emb))
|
||||
|
||||
// Create EmbeddingResp struct for the search
|
||||
embeddingResp := &models.EmbeddingResp{
|
||||
Embedding: emb,
|
||||
@@ -793,7 +793,6 @@ func chatRagUse(qText string) (string, error) {
|
||||
logger.Debug("RAG search returned vectors", "index", i, "question", q, "vector_count", len(vecs))
|
||||
respVecs = append(respVecs, vecs...)
|
||||
}
|
||||
|
||||
// get raw text
|
||||
resps := []string{}
|
||||
logger.Debug("RAG query final results", "total_vecs_found", len(respVecs))
|
||||
@@ -801,12 +800,10 @@ func chatRagUse(qText string) (string, error) {
|
||||
resps = append(resps, rv.RawText)
|
||||
logger.Debug("RAG result", "slug", rv.Slug, "filename", rv.FileName, "raw_text_len", len(rv.RawText))
|
||||
}
|
||||
|
||||
if len(resps) == 0 {
|
||||
logger.Info("No RAG results found for query", "original_query", qText, "question_count", len(questions))
|
||||
return "No related results from RAG vector storage.", nil
|
||||
}
|
||||
|
||||
result := strings.Join(resps, "\n")
|
||||
logger.Debug("RAG query completed", "result_len", len(result), "response_count", len(resps))
|
||||
return result, nil
|
||||
@@ -836,7 +833,10 @@ func chatRound(r *models.ChatRoundReq) error {
|
||||
if cfg.WriteNextMsgAsCompletionAgent != "" {
|
||||
botPersona = cfg.WriteNextMsgAsCompletionAgent
|
||||
}
|
||||
defer func() { botRespMode = false }()
|
||||
defer func() {
|
||||
botRespMode = false
|
||||
ClearImageAttachment()
|
||||
}()
|
||||
// check that there is a model set to use if is not local
|
||||
choseChunkParser()
|
||||
reader, err := chunkParser.FormMsg(r.UserMsg, r.Role, r.Resume)
|
||||
@@ -855,13 +855,14 @@ func chatRound(r *models.ChatRoundReq) error {
|
||||
chatBody.Messages = append(chatBody.Messages, models.RoleMsg{
|
||||
Role: botPersona, Content: "",
|
||||
})
|
||||
fmt.Fprintf(textView, "\n[-:-:b](%d) ", msgIdx)
|
||||
fmt.Fprint(textView, roleToIcon(botPersona))
|
||||
fmt.Fprint(textView, "[-:-:-]\n")
|
||||
if cfg.ThinkUse && !strings.Contains(cfg.CurrentAPI, "v1") {
|
||||
// fmt.Fprint(textView, "<think>")
|
||||
chunkChan <- "<think>"
|
||||
nl := "\n\n"
|
||||
prevText := textView.GetText(true)
|
||||
if strings.HasSuffix(prevText, nl) {
|
||||
nl = ""
|
||||
} else if strings.HasSuffix(prevText, "\n") {
|
||||
nl = "\n"
|
||||
}
|
||||
fmt.Fprintf(textView, "%s[-:-:b](%d) %s[-:-:-]\n", nl, msgIdx, roleToIcon(botPersona))
|
||||
} else {
|
||||
msgIdx = len(chatBody.Messages) - 1
|
||||
}
|
||||
@@ -1198,6 +1199,8 @@ func findCall(msg, toolCall string) bool {
|
||||
chatRoundChan <- crr
|
||||
return true
|
||||
}
|
||||
// Show tool call progress indicator before execution
|
||||
fmt.Fprintf(textView, "\n[yellow::i][tool: %s...][-:-:-]", fc.Name)
|
||||
resp := callToolWithAgent(fc.Name, fc.Args)
|
||||
toolMsg := string(resp) // Remove the "tool response: " prefix and %+v formatting
|
||||
logger.Info("llm used a tool call", "tool_name", fc.Name, "too_args", fc.Args, "id", fc.ID, "tool_resp", toolMsg)
|
||||
@@ -1237,7 +1240,6 @@ func chatToTextSlice(messages []models.RoleMsg, showSys bool) []string {
|
||||
func chatToText(messages []models.RoleMsg, showSys bool) string {
|
||||
s := chatToTextSlice(messages, showSys)
|
||||
text := strings.Join(s, "\n")
|
||||
|
||||
// Collapse thinking blocks if enabled
|
||||
if thinkingCollapsed {
|
||||
text = thinkRE.ReplaceAllStringFunc(text, func(match string) string {
|
||||
@@ -1261,7 +1263,6 @@ func chatToText(messages []models.RoleMsg, showSys bool) string {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ OpenRouterChatAPI = "https://openrouter.ai/api/v1/chat/completions"
|
||||
# OpenRouterToken = ""
|
||||
# embeddings
|
||||
EmbedURL = "http://localhost:8082/v1/embeddings"
|
||||
HFToken = false
|
||||
HFToken = ""
|
||||
#
|
||||
ShowSys = true
|
||||
LogFile = "log.txt"
|
||||
|
||||
@@ -18,7 +18,6 @@ type Config struct {
|
||||
UserRole string `toml:"UserRole"`
|
||||
ToolRole string `toml:"ToolRole"`
|
||||
ToolUse bool `toml:"ToolUse"`
|
||||
ThinkUse bool `toml:"ThinkUse"`
|
||||
StripThinkingFromAPI bool `toml:"StripThinkingFromAPI"`
|
||||
ReasoningEffort string `toml:"ReasoningEffort"`
|
||||
AssistantRole string `toml:"AssistantRole"`
|
||||
@@ -125,6 +124,9 @@ func LoadConfig(fn string) (*Config, error) {
|
||||
if config.CompletionAPI != "" {
|
||||
config.ApiLinks = append(config.ApiLinks, config.CompletionAPI)
|
||||
}
|
||||
if config.RAGDir == "" {
|
||||
config.RAGDir = "ragimport"
|
||||
}
|
||||
// if any value is empty fill with default
|
||||
return config, nil
|
||||
}
|
||||
|
||||
@@ -165,9 +165,6 @@ Those could be switched in program, but also bould be setup in config.
|
||||
#### ToolUse
|
||||
- Enable or disable explanation of tools to llm, so it could use them.
|
||||
|
||||
#### ThinkUse
|
||||
- Enable or disable insertion of JsonSerializerToken at the beggining of llm resp.
|
||||
|
||||
### StripThinkingFromAPI (`true`)
|
||||
- Strip thinking blocks from messages before sending to LLM. Keeps them in chat history for local viewing but reduces token usage in API calls.
|
||||
|
||||
|
||||
9
llm.go
9
llm.go
@@ -184,9 +184,6 @@ func (lcp LCPCompletion) FormMsg(msg, role string, resume bool) (io.Reader, erro
|
||||
botMsgStart := "\n" + botPersona + ":\n"
|
||||
prompt += botMsgStart
|
||||
}
|
||||
if cfg.ThinkUse && !cfg.ToolUse {
|
||||
prompt += "<think>"
|
||||
}
|
||||
logger.Debug("checking prompt for /completion", "tool_use", cfg.ToolUse,
|
||||
"msg", msg, "resume", resume, "prompt", prompt, "multimodal_data_count", len(multimodalData))
|
||||
payload := models.NewLCPReq(prompt, chatBody.Model, multimodalData,
|
||||
@@ -423,9 +420,6 @@ func (ds DeepSeekerCompletion) FormMsg(msg, role string, resume bool) (io.Reader
|
||||
botMsgStart := "\n" + botPersona + ":\n"
|
||||
prompt += botMsgStart
|
||||
}
|
||||
if cfg.ThinkUse && !cfg.ToolUse {
|
||||
prompt += "<think>"
|
||||
}
|
||||
logger.Debug("checking prompt for /completion", "tool_use", cfg.ToolUse,
|
||||
"msg", msg, "resume", resume, "prompt", prompt)
|
||||
payload := models.NewDSCompletionReq(prompt, chatBody.Model,
|
||||
@@ -589,9 +583,6 @@ func (or OpenRouterCompletion) FormMsg(msg, role string, resume bool) (io.Reader
|
||||
botMsgStart := "\n" + botPersona + ":\n"
|
||||
prompt += botMsgStart
|
||||
}
|
||||
if cfg.ThinkUse && !cfg.ToolUse {
|
||||
prompt += "<think>"
|
||||
}
|
||||
stopSlice := chatBody.MakeStopSliceExcluding("", listChatRoles())
|
||||
logger.Debug("checking prompt for /completion", "tool_use", cfg.ToolUse,
|
||||
"msg", msg, "resume", resume, "prompt", prompt, "stop_strings", stopSlice)
|
||||
|
||||
@@ -241,8 +241,7 @@ func (m *RoleMsg) ToText(i int) string {
|
||||
}
|
||||
finalContent.WriteString(contentStr)
|
||||
if m.Stats != nil {
|
||||
finalContent.WriteString(fmt.Sprintf("\n[gray::i][%d tok, %.1fs, %.1f t/s][-:-:-]",
|
||||
m.Stats.Tokens, m.Stats.Duration, m.Stats.TokensPerSec))
|
||||
fmt.Fprintf(&finalContent, "\n[gray::i][%d tok, %.1fs, %.1f t/s][-:-:-]", m.Stats.Tokens, m.Stats.Duration, m.Stats.TokensPerSec)
|
||||
}
|
||||
textMsg := fmt.Sprintf("[-:-:b]%s[-:-:-]\n%s\n", icon, finalContent.String())
|
||||
return strings.ReplaceAll(textMsg, "\n\n", "\n")
|
||||
|
||||
20
popups.go
20
popups.go
@@ -51,7 +51,7 @@ func showModelSelectionPopup() {
|
||||
// Find the current model index to set as selected
|
||||
currentModelIndex := -1
|
||||
for i, model := range modelList {
|
||||
if model == chatBody.Model {
|
||||
if strings.TrimPrefix(model, "(loaded) ") == chatBody.Model {
|
||||
currentModelIndex = i
|
||||
}
|
||||
modelListWidget.AddItem(model, "", 0, nil)
|
||||
@@ -65,16 +65,19 @@ func showModelSelectionPopup() {
|
||||
chatBody.Model = modelName
|
||||
cfg.CurrentModel = chatBody.Model
|
||||
pages.RemovePage("modelSelectionPopup")
|
||||
app.SetFocus(textArea)
|
||||
updateCachedModelColor()
|
||||
updateStatusLine()
|
||||
})
|
||||
modelListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
pages.RemovePage("modelSelectionPopup")
|
||||
app.SetFocus(textArea)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
||||
pages.RemovePage("modelSelectionPopup")
|
||||
app.SetFocus(textArea)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
@@ -160,6 +163,7 @@ func showAPILinkSelectionPopup() {
|
||||
cfg.CurrentModel = chatBody.Model
|
||||
}
|
||||
pages.RemovePage("apiLinkSelectionPopup")
|
||||
app.SetFocus(textArea)
|
||||
choseChunkParser()
|
||||
updateCachedModelColor()
|
||||
updateStatusLine()
|
||||
@@ -167,10 +171,12 @@ func showAPILinkSelectionPopup() {
|
||||
apiListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
pages.RemovePage("apiLinkSelectionPopup")
|
||||
app.SetFocus(textArea)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
||||
pages.RemovePage("apiLinkSelectionPopup")
|
||||
app.SetFocus(textArea)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
@@ -230,6 +236,7 @@ func showUserRoleSelectionPopup() {
|
||||
textView.SetText(chatToText(filtered, cfg.ShowSys))
|
||||
// Remove the popup page
|
||||
pages.RemovePage("userRoleSelectionPopup")
|
||||
app.SetFocus(textArea)
|
||||
// Update the status line to reflect the change
|
||||
updateStatusLine()
|
||||
colorText()
|
||||
@@ -237,10 +244,12 @@ func showUserRoleSelectionPopup() {
|
||||
roleListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
pages.RemovePage("userRoleSelectionPopup")
|
||||
app.SetFocus(textArea)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
||||
pages.RemovePage("userRoleSelectionPopup")
|
||||
app.SetFocus(textArea)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
@@ -303,16 +312,19 @@ func showBotRoleSelectionPopup() {
|
||||
cfg.WriteNextMsgAsCompletionAgent = mainText
|
||||
// Remove the popup page
|
||||
pages.RemovePage("botRoleSelectionPopup")
|
||||
app.SetFocus(textArea)
|
||||
// Update the status line to reflect the change
|
||||
updateStatusLine()
|
||||
})
|
||||
roleListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
pages.RemovePage("botRoleSelectionPopup")
|
||||
app.SetFocus(textArea)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
||||
pages.RemovePage("botRoleSelectionPopup")
|
||||
app.SetFocus(textArea)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
@@ -364,14 +376,17 @@ func showFileCompletionPopup(filter string) {
|
||||
textArea.SetText(before+mainText, true)
|
||||
}
|
||||
pages.RemovePage("fileCompletionPopup")
|
||||
app.SetFocus(textArea)
|
||||
})
|
||||
widget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
pages.RemovePage("fileCompletionPopup")
|
||||
app.SetFocus(textArea)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
||||
pages.RemovePage("fileCompletionPopup")
|
||||
app.SetFocus(textArea)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
@@ -484,14 +499,17 @@ func showColorschemeSelectionPopup() {
|
||||
}
|
||||
// Remove the popup page
|
||||
pages.RemovePage("colorschemeSelectionPopup")
|
||||
app.SetFocus(textArea)
|
||||
})
|
||||
schemeListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
pages.RemovePage("colorschemeSelectionPopup")
|
||||
app.SetFocus(textArea)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
||||
pages.RemovePage("colorschemeSelectionPopup")
|
||||
app.SetFocus(textArea)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
|
||||
@@ -115,9 +115,6 @@ func makePropsTable(props map[string]float32) *tview.Table {
|
||||
row++
|
||||
}
|
||||
// Add checkboxes
|
||||
addCheckboxRow("Insert <think> tag (/completion only)", cfg.ThinkUse, func(checked bool) {
|
||||
cfg.ThinkUse = checked
|
||||
})
|
||||
addCheckboxRow("RAG use", cfg.RAGEnabled, func(checked bool) {
|
||||
cfg.RAGEnabled = checked
|
||||
})
|
||||
|
||||
27
tui.go
27
tui.go
@@ -15,6 +15,11 @@ import (
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
func isFullScreenPageActive() bool {
|
||||
name, _ := pages.GetFrontPage()
|
||||
return name != "main"
|
||||
}
|
||||
|
||||
var (
|
||||
app *tview.Application
|
||||
pages *tview.Pages
|
||||
@@ -525,6 +530,9 @@ func init() {
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyRune && event.Rune() == 'i' && event.Modifiers()&tcell.ModAlt != 0 {
|
||||
if isFullScreenPageActive() {
|
||||
return event
|
||||
}
|
||||
showColorschemeSelectionPopup()
|
||||
return nil
|
||||
}
|
||||
@@ -731,6 +739,9 @@ func init() {
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyCtrlL {
|
||||
if isFullScreenPageActive() {
|
||||
return event
|
||||
}
|
||||
// Show model selection popup instead of rotating models
|
||||
showModelSelectionPopup()
|
||||
return nil
|
||||
@@ -744,6 +755,9 @@ func init() {
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyCtrlV {
|
||||
if isFullScreenPageActive() {
|
||||
return event
|
||||
}
|
||||
// Show API link selection popup instead of rotating APIs
|
||||
showAPILinkSelectionPopup()
|
||||
return nil
|
||||
@@ -850,11 +864,17 @@ func init() {
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyCtrlQ {
|
||||
if isFullScreenPageActive() {
|
||||
return event
|
||||
}
|
||||
// Show user role selection popup instead of cycling through roles
|
||||
showUserRoleSelectionPopup()
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyCtrlX {
|
||||
if isFullScreenPageActive() {
|
||||
return event
|
||||
}
|
||||
// Show bot role selection popup instead of cycling through roles
|
||||
showBotRoleSelectionPopup()
|
||||
return nil
|
||||
@@ -975,13 +995,6 @@ func init() {
|
||||
}
|
||||
// go chatRound(msgText, persona, textView, false, false)
|
||||
chatRoundChan <- &models.ChatRoundReq{Role: persona, UserMsg: msgText}
|
||||
// Also clear any image attachment after sending the message
|
||||
go func() {
|
||||
// Wait a short moment for the message to be processed, then clear the image attachment
|
||||
// This allows the image to be sent with the current message if it was attached
|
||||
// But clears it for the next message
|
||||
ClearImageAttachment()
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user