From 82ffc364d34f4906ef4c4c1bd4bd202d393a46bc Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Sun, 14 Dec 2025 13:34:26 +0300 Subject: Chore: remove unused ticker --- bot.go | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) (limited to 'bot.go') diff --git a/bot.go b/bot.go index 3242b88..e2f03b8 100644 --- a/bot.go +++ b/bot.go @@ -841,15 +841,6 @@ func updateModelLists() { } } -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") @@ -910,5 +901,5 @@ func init() { if cfg.STT_ENABLED { asr = extra.NewSTT(logger, cfg) } - go updateModelListsTicker() + go updateModelLists() } -- cgit v1.2.3 From d73c3abd6bda8690e8b5e57342221c8cb2cc88b3 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Wed, 17 Dec 2025 13:03:40 +0300 Subject: Feat: preload lcp model --- bot.go | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) (limited to 'bot.go') diff --git a/bot.go b/bot.go index e2f03b8..8a0ba0a 100644 --- a/bot.go +++ b/bot.go @@ -16,6 +16,7 @@ import ( "log/slog" "net" "net/http" + "net/url" "os" "path" "strings" @@ -188,6 +189,58 @@ func createClient(connectTimeout time.Duration) *http.Client { } } +func warmUpModel() { + u, err := url.Parse(cfg.CurrentAPI) + if err != nil { + return + } + host := u.Hostname() + if host != "localhost" && host != "127.0.0.1" && host != "::1" { + return + } + go func() { + var data []byte + var err error + if strings.HasSuffix(cfg.CurrentAPI, "/completion") { + // Old completion endpoint + req := models.NewLCPReq(".", chatBody.Model, nil, map[string]float32{ + "temperature": 0.8, + "dry_multiplier": 0.0, + "min_p": 0.05, + "n_predict": 0, + }, []string{}) + req.Stream = false + data, err = json.Marshal(req) + } else if strings.Contains(cfg.CurrentAPI, "/v1/chat/completions") { + // OpenAI-compatible chat endpoint + req := models.OpenAIReq{ + ChatBody: &models.ChatBody{ + Model: chatBody.Model, + Messages: []models.RoleMsg{ + {Role: "system", Content: "."}, + }, + Stream: false, + }, + Tools: nil, + } + data, err = json.Marshal(req) + } else { + // Unknown local endpoint, skip + return + } + if err != nil { + logger.Debug("failed to marshal warmup request", "error", err) + return + } + resp, err := httpClient.Post(cfg.CurrentAPI, "application/json", bytes.NewReader(data)) + if err != nil { + logger.Debug("warmup request failed", "error", err) + return + } + resp.Body.Close() + }() +} + func fetchLCPModelName() *models.LCPModels { //nolint resp, err := httpClient.Get(cfg.FetchModelNameAPI) @@ -894,7 +947,7 @@ func init() { cluedoState = extra.CluedoPrepCards(playerOrder) } choseChunkParser() - httpClient = createClient(time.Second * 15) + httpClient = createClient(time.Second * 90) if cfg.TTS_ENABLED { orator = extra.NewOrator(logger, cfg) } -- cgit v1.2.3 From a06cfd995f05782854844e51a71a656f70274f64 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Thu, 18 Dec 2025 11:53:07 +0300 Subject: Feat: add agent entity --- bot.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'bot.go') diff --git a/bot.go b/bot.go index 8a0ba0a..ddc6d2a 100644 --- a/bot.go +++ b/bot.go @@ -756,7 +756,7 @@ func findCall(msg, toolCall string, tv *tview.TextView) { } } // call a func - f, ok := fnMap[fc.Name] + _, ok := fnMap[fc.Name] if !ok { m := fc.Name + " is not implemented" // Create tool response message with the proper tool_call_id @@ -775,7 +775,7 @@ func findCall(msg, toolCall string, tv *tview.TextView) { chatRound("", cfg.AssistantRole, tv, false, false) return } - resp := f(fc.Args) + resp := callToolWithAgent(fc.Name, fc.Args) toolMsg := string(resp) // Remove the "tool response: " prefix and %+v formatting logger.Info("llm used tool call", "tool_resp", toolMsg, "tool_attrs", fc) fmt.Fprintf(tv, "%s[-:-:b](%d) <%s>: [-:-:-]\n%s\n", -- cgit v1.2.3 From 8cdec5e54455c3dfb74c2e8016f17f806f86fa54 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Thu, 18 Dec 2025 14:39:06 +0300 Subject: Feat: http request for agent --- bot.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'bot.go') diff --git a/bot.go b/bot.go index ddc6d2a..d6418ff 100644 --- a/bot.go +++ b/bot.go @@ -327,12 +327,11 @@ func fetchLCPModels() ([]string, error) { return localModels, nil } +// sendMsgToLLM expects streaming resp func sendMsgToLLM(body io.Reader) { choseChunkParser() - var req *http.Request var err error - // Capture and log the request body for debugging if _, ok := body.(*io.LimitedReader); ok { // If it's a LimitedReader, we need to handle it differently @@ -379,7 +378,6 @@ func sendMsgToLLM(body io.Reader) { return } } - req.Header.Add("Accept", "application/json") req.Header.Add("Content-Type", "application/json") req.Header.Add("Authorization", "Bearer "+chunkParser.GetToken()) -- cgit v1.2.3 From 5f852418d8d12868df83a9591b15e0846971fff9 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Thu, 18 Dec 2025 15:22:03 +0300 Subject: Chore: request cleanup --- bot.go | 62 +++++++++++--------------------------------------------------- 1 file changed, 11 insertions(+), 51 deletions(-) (limited to 'bot.go') diff --git a/bot.go b/bot.go index d6418ff..f2683bb 100644 --- a/bot.go +++ b/bot.go @@ -330,59 +330,19 @@ func fetchLCPModels() ([]string, error) { // sendMsgToLLM expects streaming resp func sendMsgToLLM(body io.Reader) { choseChunkParser() - var req *http.Request - var err error - // Capture and log the request body for debugging - if _, ok := body.(*io.LimitedReader); ok { - // If it's a LimitedReader, we need to handle it differently - logger.Debug("request body type is LimitedReader", "parser", chunkParser, "link", cfg.CurrentAPI) - req, err = http.NewRequest("POST", cfg.CurrentAPI, body) - if err != nil { - logger.Error("newreq error", "error", err) - if err := notifyUser("error", "apicall failed:"+err.Error()); err != nil { - logger.Error("failed to notify", "error", err) - } - streamDone <- true - return - } - req.Header.Add("Accept", "application/json") - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Authorization", "Bearer "+chunkParser.GetToken()) - req.Header.Set("Accept-Encoding", "gzip") - } else { - // For other reader types, capture and log the body content - bodyBytes, err := io.ReadAll(body) - if err != nil { - logger.Error("failed to read request body for logging", "error", err) - // Create request with original body if reading fails - req, err = http.NewRequest("POST", cfg.CurrentAPI, bytes.NewReader(bodyBytes)) - if err != nil { - logger.Error("newreq error", "error", err) - if err := notifyUser("error", "apicall failed:"+err.Error()); err != nil { - logger.Error("failed to notify", "error", err) - } - streamDone <- true - return - } - } else { - // Log the request body for debugging - logger.Debug("sending request to API", "api", cfg.CurrentAPI, "body", string(bodyBytes)) - // Create request with the captured body - req, err = http.NewRequest("POST", cfg.CurrentAPI, bytes.NewReader(bodyBytes)) - if err != nil { - logger.Error("newreq error", "error", err) - if err := notifyUser("error", "apicall failed:"+err.Error()); err != nil { - logger.Error("failed to notify", "error", err) - } - streamDone <- true - return - } + req, err := http.NewRequest("POST", cfg.CurrentAPI, body) + if err != nil { + logger.Error("newreq error", "error", err) + if err := notifyUser("error", "apicall failed:"+err.Error()); err != nil { + logger.Error("failed to notify", "error", err) } - req.Header.Add("Accept", "application/json") - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Authorization", "Bearer "+chunkParser.GetToken()) - req.Header.Set("Accept-Encoding", "gzip") + streamDone <- true + return } + req.Header.Add("Accept", "application/json") + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", "Bearer "+chunkParser.GetToken()) + req.Header.Set("Accept-Encoding", "gzip") // nolint resp, err := httpClient.Do(req) if err != nil { -- cgit v1.2.3 From 67ea1aef0dafb9dc6f82e009cc1ecc613f71e520 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Fri, 19 Dec 2025 11:06:22 +0300 Subject: Feat: two agent types; WebAgentB impl --- bot.go | 1 + 1 file changed, 1 insertion(+) (limited to 'bot.go') diff --git a/bot.go b/bot.go index f2683bb..8206c63 100644 --- a/bot.go +++ b/bot.go @@ -263,6 +263,7 @@ func fetchLCPModelName() *models.LCPModels { return nil } chatBody.Model = path.Base(llmModel.Data[0].ID) + cfg.CurrentModel = chatBody.Model return &llmModel } -- cgit v1.2.3 From f779f039745f97f08f25967214d07716ce213326 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Fri, 19 Dec 2025 15:39:55 +0300 Subject: Enha: agent request builder --- bot.go | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 4 deletions(-) (limited to 'bot.go') diff --git a/bot.go b/bot.go index 8206c63..779278e 100644 --- a/bot.go +++ b/bot.go @@ -6,6 +6,7 @@ import ( "context" "encoding/json" "fmt" + "strconv" "gf-lt/config" "gf-lt/extra" "gf-lt/models" @@ -659,14 +660,75 @@ func cleanChatBody() { } } +// convertJSONToMapStringString unmarshals JSON into map[string]interface{} and converts all values to strings. +func convertJSONToMapStringString(jsonStr string) (map[string]string, error) { + var raw map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &raw); err != nil { + return nil, err + } + result := make(map[string]string, len(raw)) + for k, v := range raw { + switch val := v.(type) { + case string: + result[k] = val + case float64: + result[k] = strconv.FormatFloat(val, 'f', -1, 64) + case int, int64, int32: + // json.Unmarshal converts numbers to float64, but handle other integer types if they appear + result[k] = fmt.Sprintf("%v", val) + case bool: + result[k] = strconv.FormatBool(val) + case nil: + result[k] = "" + default: + result[k] = fmt.Sprintf("%v", val) + } + } + return result, nil +} + +// unmarshalFuncCall unmarshals a JSON tool call, converting numeric arguments to strings. +func unmarshalFuncCall(jsonStr string) (*models.FuncCall, error) { + type tempFuncCall struct { + ID string `json:"id,omitempty"` + Name string `json:"name"` + Args map[string]interface{} `json:"args"` + } + var temp tempFuncCall + if err := json.Unmarshal([]byte(jsonStr), &temp); err != nil { + return nil, err + } + fc := &models.FuncCall{ + ID: temp.ID, + Name: temp.Name, + Args: make(map[string]string, len(temp.Args)), + } + for k, v := range temp.Args { + switch val := v.(type) { + case string: + fc.Args[k] = val + case float64: + fc.Args[k] = strconv.FormatFloat(val, 'f', -1, 64) + case int, int64, int32: + fc.Args[k] = fmt.Sprintf("%v", val) + case bool: + fc.Args[k] = strconv.FormatBool(val) + case nil: + fc.Args[k] = "" + default: + fc.Args[k] = fmt.Sprintf("%v", val) + } + } + return fc, nil +} + func findCall(msg, toolCall string, tv *tview.TextView) { fc := &models.FuncCall{} if toolCall != "" { // HTML-decode the tool call string to handle encoded characters like < -> <= decodedToolCall := html.UnescapeString(toolCall) - openAIToolMap := make(map[string]string) - // respect tool call - if err := json.Unmarshal([]byte(decodedToolCall), &openAIToolMap); err != nil { + openAIToolMap, err := convertJSONToMapStringString(decodedToolCall) + if err != nil { logger.Error("failed to unmarshal openai tool call", "call", decodedToolCall, "error", err) // Send error response to LLM so it can retry or handle the error toolResponseMsg := models.RoleMsg{ @@ -700,7 +762,9 @@ func findCall(msg, toolCall string, tv *tview.TextView) { jsStr = strings.TrimSuffix(strings.TrimPrefix(jsStr, prefix), suffix) // HTML-decode the JSON string to handle encoded characters like < -> <= decodedJsStr := html.UnescapeString(jsStr) - if err := json.Unmarshal([]byte(decodedJsStr), &fc); err != nil { + var err error + fc, err = unmarshalFuncCall(decodedJsStr) + if err != nil { logger.Error("failed to unmarshal tool call", "error", err, "json_string", decodedJsStr) // Send error response to LLM so it can retry or handle the error toolResponseMsg := models.RoleMsg{ -- cgit v1.2.3 From 12f11388e72c4ce3314055d777f3034e31cdea77 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Fri, 19 Dec 2025 16:14:24 +0300 Subject: Feat: notify on load --- bot.go | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) (limited to 'bot.go') diff --git a/bot.go b/bot.go index 779278e..1603e0d 100644 --- a/bot.go +++ b/bot.go @@ -199,6 +199,18 @@ func warmUpModel() { if host != "localhost" && host != "127.0.0.1" && host != "::1" { return } + // Check if model is already loaded + loaded, err := isModelLoaded(chatBody.Model) + if err != nil { + logger.Debug("failed to check model status", "model", chatBody.Model, "error", err) + // Continue with warmup attempt anyway + } + if loaded { + if err := notifyUser("model already loaded", "Model "+chatBody.Model+" is already loaded."); err != nil { + logger.Debug("failed to notify user", "error", err) + } + return + } go func() { var data []byte var err error @@ -239,6 +251,8 @@ func warmUpModel() { return } resp.Body.Close() + // Start monitoring for model load completion + monitorModelLoad(chatBody.Model) }() } @@ -329,6 +343,66 @@ func fetchLCPModels() ([]string, error) { return localModels, nil } +// fetchLCPModelsWithStatus returns the full LCPModels struct including status information. +func fetchLCPModelsWithStatus() (*models.LCPModels, error) { + resp, err := http.Get(cfg.FetchModelNameAPI) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + err := fmt.Errorf("failed to fetch llama.cpp models; status: %s", resp.Status) + return nil, err + } + data := &models.LCPModels{} + if err := json.NewDecoder(resp.Body).Decode(data); err != nil { + return nil, err + } + return data, nil +} + +// isModelLoaded checks if the given model ID is currently loaded in llama.cpp server. +func isModelLoaded(modelID string) (bool, error) { + models, err := fetchLCPModelsWithStatus() + if err != nil { + return false, err + } + for _, m := range models.Data { + if m.ID == modelID { + return m.Status.Value == "loaded", nil + } + } + return false, nil +} + +// monitorModelLoad starts a goroutine that periodically checks if the specified model is loaded. +func monitorModelLoad(modelID string) { + go func() { + timeout := time.After(2 * time.Minute) // max wait 2 minutes + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + for { + select { + case <-timeout: + logger.Debug("model load monitoring timeout", "model", modelID) + return + case <-ticker.C: + loaded, err := isModelLoaded(modelID) + if err != nil { + logger.Debug("failed to check model status", "model", modelID, "error", err) + continue + } + if loaded { + if err := notifyUser("model loaded", "Model "+modelID+" is now loaded and ready."); err != nil { + logger.Debug("failed to notify user", "error", err) + } + return + } + } + } + }() +} + // sendMsgToLLM expects streaming resp func sendMsgToLLM(body io.Reader) { choseChunkParser() -- cgit v1.2.3 From 8c18b1b74cd53584988ab8cd55e50be81aa9aca5 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Sat, 20 Dec 2025 11:04:57 +0300 Subject: Enha: remove old tool calls --- bot.go | 62 ++++++++++++++++++++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 24 deletions(-) (limited to 'bot.go') diff --git a/bot.go b/bot.go index 1603e0d..054c310 100644 --- a/bot.go +++ b/bot.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" "fmt" - "strconv" "gf-lt/config" "gf-lt/extra" "gf-lt/models" @@ -20,6 +19,7 @@ import ( "net/url" "os" "path" + "strconv" "strings" "time" @@ -86,19 +86,31 @@ func cleanNullMessages(messages []models.RoleMsg) []models.RoleMsg { return consolidateConsecutiveAssistantMessages(messages) } +func cleanToolCalls(messages []models.RoleMsg) []models.RoleMsg { + cleaned := make([]models.RoleMsg, 0, len(messages)) + for i, msg := range messages { + // recognize the message as the tool call and remove it + if msg.ToolCallID == "" { + cleaned = append(cleaned, msg) + } + // tool call in last msg should stay + if i == len(messages)-1 { + cleaned = append(cleaned, msg) + } + } + return consolidateConsecutiveAssistantMessages(cleaned) +} + // consolidateConsecutiveAssistantMessages merges consecutive assistant messages into a single message func consolidateConsecutiveAssistantMessages(messages []models.RoleMsg) []models.RoleMsg { if len(messages) == 0 { return messages } - consolidated := make([]models.RoleMsg, 0, len(messages)) currentAssistantMsg := models.RoleMsg{} isBuildingAssistantMsg := false - for i := 0; i < len(messages); i++ { msg := messages[i] - if msg.Role == cfg.AssistantRole || msg.Role == cfg.WriteNextMsgAsCompletionAgent { // If this is an assistant message, start or continue building if !isBuildingAssistantMsg { @@ -143,12 +155,10 @@ func consolidateConsecutiveAssistantMessages(messages []models.RoleMsg) []models consolidated = append(consolidated, msg) } } - // Don't forget the last assistant message if we were building one if isBuildingAssistantMsg { consolidated = append(consolidated, currentAssistantMsg) } - return consolidated } @@ -483,6 +493,7 @@ func sendMsgToLLM(body io.Reader) { streamDone <- true break } + // // problem: this catches any mention of the word 'error' // Handle error messages in response content // example needed, since llm could use the word error in the normal msg // if string(line) != "" && strings.Contains(strings.ToLower(string(line)), "error") { @@ -691,20 +702,16 @@ out: Role: botPersona, Content: respText.String(), }) } - logger.Debug("chatRound: before cleanChatBody", "messages_before_clean", len(chatBody.Messages)) for i, msg := range chatBody.Messages { logger.Debug("chatRound: before cleaning", "index", i, "role", msg.Role, "content_len", len(msg.Content), "has_content", msg.HasContent(), "tool_call_id", msg.ToolCallID) } - // // Clean null/empty messages to prevent API issues with endpoints like llama.cpp jinja template cleanChatBody() - logger.Debug("chatRound: after cleanChatBody", "messages_after_clean", len(chatBody.Messages)) for i, msg := range chatBody.Messages { logger.Debug("chatRound: after cleaning", "index", i, "role", msg.Role, "content_len", len(msg.Content), "has_content", msg.HasContent(), "tool_call_id", msg.ToolCallID) } - colorText() updateStatusLine() // bot msg is done; @@ -718,19 +725,19 @@ out: // cleanChatBody removes messages with null or empty content to prevent API issues func cleanChatBody() { - if chatBody != nil && chatBody.Messages != nil { - originalLen := len(chatBody.Messages) - logger.Debug("cleanChatBody: before cleaning", "message_count", originalLen) - for i, msg := range chatBody.Messages { - logger.Debug("cleanChatBody: before clean", "index", i, "role", msg.Role, "content_len", len(msg.Content), "has_content", msg.HasContent(), "tool_call_id", msg.ToolCallID) - } - - chatBody.Messages = cleanNullMessages(chatBody.Messages) - - logger.Debug("cleanChatBody: after cleaning", "original_len", originalLen, "new_len", len(chatBody.Messages)) - for i, msg := range chatBody.Messages { - logger.Debug("cleanChatBody: after clean", "index", i, "role", msg.Role, "content_len", len(msg.Content), "has_content", msg.HasContent(), "tool_call_id", msg.ToolCallID) - } + if chatBody == nil || chatBody.Messages == nil { + return + } + originalLen := len(chatBody.Messages) + logger.Debug("cleanChatBody: before cleaning", "message_count", originalLen) + for i, msg := range chatBody.Messages { + logger.Debug("cleanChatBody: before clean", "index", i, "role", msg.Role, "content_len", len(msg.Content), "has_content", msg.HasContent(), "tool_call_id", msg.ToolCallID) + } + chatBody.Messages = cleanToolCalls(chatBody.Messages) + chatBody.Messages = cleanNullMessages(chatBody.Messages) + logger.Debug("cleanChatBody: after cleaning", "original_len", originalLen, "new_len", len(chatBody.Messages)) + for i, msg := range chatBody.Messages { + logger.Debug("cleanChatBody: after clean", "index", i, "role", msg.Role, "content_len", len(msg.Content), "has_content", msg.HasContent(), "tool_call_id", msg.ToolCallID) } } @@ -852,6 +859,14 @@ func findCall(msg, toolCall string, tv *tview.TextView) { return } } + // we got here => last msg recognized as a tool call (correct or not) + // make sure it has ToolCallID + if chatBody.Messages[len(chatBody.Messages)-1].ToolCallID == "" { + chatBody.Messages[len(chatBody.Messages)-1].ToolCallID = randString(6) + } + if lastToolCallID == "" { + lastToolCallID = chatBody.Messages[len(chatBody.Messages)-1].ToolCallID + } // call a func _, ok := fnMap[fc.Name] if !ok { @@ -866,7 +881,6 @@ func findCall(msg, toolCall string, tv *tview.TextView) { logger.Debug("findCall: added tool not implemented response", "role", toolResponseMsg.Role, "content_len", len(toolResponseMsg.Content), "tool_call_id", toolResponseMsg.ToolCallID, "message_count_after_add", len(chatBody.Messages)) // Clear the stored tool call ID after using it lastToolCallID = "" - // Trigger the assistant to continue processing with the new tool response // by calling chatRound with empty content to continue the assistant's response chatRound("", cfg.AssistantRole, tv, false, false) -- cgit v1.2.3 From 0ca709b7c679c641724a3a8c2fc1425286b4955a Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Sat, 20 Dec 2025 11:34:02 +0300 Subject: Enha: update lasttoolcallid --- bot.go | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) (limited to 'bot.go') diff --git a/bot.go b/bot.go index 054c310..d84cfad 100644 --- a/bot.go +++ b/bot.go @@ -49,7 +49,6 @@ var ( ragger *rag.RAG chunkParser ChunkParser lastToolCall *models.FuncCall - lastToolCallID string // Store the ID of the most recent tool call //nolint:unused // TTS_ENABLED conditionally uses this orator extra.Orator asr extra.STT @@ -520,7 +519,7 @@ func sendMsgToLLM(body io.Reader) { if chunk.FuncName != "" { lastToolCall.Name = chunk.FuncName // Store the tool call ID for the response - lastToolCallID = chunk.ToolID + lastToolCall.ID = chunk.ToolID } interrupt: if interruptResp { // read bytes, so it would not get into beginning of the next req @@ -811,26 +810,25 @@ func findCall(msg, toolCall string, tv *tview.TextView) { openAIToolMap, err := convertJSONToMapStringString(decodedToolCall) if err != nil { logger.Error("failed to unmarshal openai tool call", "call", decodedToolCall, "error", err) + // Ensure lastToolCall.ID is set for the error response (already set from chunk) // Send error response to LLM so it can retry or handle the error toolResponseMsg := models.RoleMsg{ Role: cfg.ToolRole, Content: fmt.Sprintf("Error processing tool call: %v. Please check the JSON format and try again.", err), - ToolCallID: lastToolCallID, // Use the stored tool call ID + ToolCallID: lastToolCall.ID, // Use the stored tool call ID } chatBody.Messages = append(chatBody.Messages, toolResponseMsg) - // Clear the stored tool call ID after using it - lastToolCallID = "" + // Clear the stored tool call ID after using it (no longer needed) // Trigger the assistant to continue processing with the error message chatRound("", cfg.AssistantRole, tv, false, false) return } lastToolCall.Args = openAIToolMap fc = lastToolCall - // Ensure lastToolCallID is set if it's available in the tool call - if lastToolCallID == "" && len(openAIToolMap) > 0 { - // Attempt to extract ID from the parsed tool call if not already set + // Set lastToolCall.ID from parsed tool call ID if available + if len(openAIToolMap) > 0 { if id, exists := openAIToolMap["id"]; exists { - lastToolCallID = id + lastToolCall.ID = id } } } else { @@ -858,14 +856,19 @@ func findCall(msg, toolCall string, tv *tview.TextView) { chatRound("", cfg.AssistantRole, tv, false, false) return } + // Update lastToolCall with parsed function call + lastToolCall.ID = fc.ID + lastToolCall.Name = fc.Name + lastToolCall.Args = fc.Args } // we got here => last msg recognized as a tool call (correct or not) // make sure it has ToolCallID if chatBody.Messages[len(chatBody.Messages)-1].ToolCallID == "" { chatBody.Messages[len(chatBody.Messages)-1].ToolCallID = randString(6) } - if lastToolCallID == "" { - lastToolCallID = chatBody.Messages[len(chatBody.Messages)-1].ToolCallID + // Ensure lastToolCall.ID is set, fallback to assistant message's ToolCallID + if lastToolCall.ID == "" { + lastToolCall.ID = chatBody.Messages[len(chatBody.Messages)-1].ToolCallID } // call a func _, ok := fnMap[fc.Name] @@ -875,12 +878,12 @@ func findCall(msg, toolCall string, tv *tview.TextView) { toolResponseMsg := models.RoleMsg{ Role: cfg.ToolRole, Content: m, - ToolCallID: lastToolCallID, // Use the stored tool call ID + ToolCallID: lastToolCall.ID, // Use the stored tool call ID } chatBody.Messages = append(chatBody.Messages, toolResponseMsg) logger.Debug("findCall: added tool not implemented response", "role", toolResponseMsg.Role, "content_len", len(toolResponseMsg.Content), "tool_call_id", toolResponseMsg.ToolCallID, "message_count_after_add", len(chatBody.Messages)) // Clear the stored tool call ID after using it - lastToolCallID = "" + lastToolCall.ID = "" // Trigger the assistant to continue processing with the new tool response // by calling chatRound with empty content to continue the assistant's response chatRound("", cfg.AssistantRole, tv, false, false) @@ -895,12 +898,12 @@ func findCall(msg, toolCall string, tv *tview.TextView) { toolResponseMsg := models.RoleMsg{ Role: cfg.ToolRole, Content: toolMsg, - ToolCallID: lastToolCallID, // Use the stored tool call ID + ToolCallID: lastToolCall.ID, // Use the stored tool call ID } chatBody.Messages = append(chatBody.Messages, toolResponseMsg) logger.Debug("findCall: added actual tool response", "role", toolResponseMsg.Role, "content_len", len(toolResponseMsg.Content), "tool_call_id", toolResponseMsg.ToolCallID, "message_count_after_add", len(chatBody.Messages)) // Clear the stored tool call ID after using it - lastToolCallID = "" + lastToolCall.ID = "" // Trigger the assistant to continue processing with the new tool response // by calling chatRound with empty content to continue the assistant's response chatRound("", cfg.AssistantRole, tv, false, false) -- cgit v1.2.3 From ba3330ee54bcab5cfde470f8e465fc9ed1c6cb2c Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Sat, 20 Dec 2025 14:21:40 +0300 Subject: Fix: model load if llama.cpp started after gf-lt --- bot.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) (limited to 'bot.go') diff --git a/bot.go b/bot.go index d84cfad..7c8ea75 100644 --- a/bot.go +++ b/bot.go @@ -21,6 +21,7 @@ import ( "path" "strconv" "strings" + "sync" "time" "github.com/neurosnap/sentences/english" @@ -52,6 +53,7 @@ var ( //nolint:unused // TTS_ENABLED conditionally uses this orator extra.Orator asr extra.STT + localModelsMu sync.RWMutex defaultLCPProps = map[string]float32{ "temperature": 0.8, "dry_multiplier": 0.0, @@ -1002,12 +1004,32 @@ func updateModelLists() { } } // if llama.cpp started after gf-lt? + localModelsMu.Lock() LocalModels, err = fetchLCPModels() + localModelsMu.Unlock() if err != nil { logger.Warn("failed to fetch llama.cpp models", "error", err) } } +func refreshLocalModelsIfEmpty() { + localModelsMu.RLock() + if len(LocalModels) > 0 { + localModelsMu.RUnlock() + return + } + localModelsMu.RUnlock() + // try to fetch + models, err := fetchLCPModels() + if err != nil { + logger.Warn("failed to fetch llama.cpp models", "error", err) + return + } + localModelsMu.Lock() + LocalModels = models + localModelsMu.Unlock() +} + func init() { var err error cfg, err = config.LoadConfig("config.toml") -- cgit v1.2.3 From 5525c946613a6f726cd116d79f1505a63ab25806 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Sun, 21 Dec 2025 09:46:07 +0300 Subject: Chore: add empty line between messages --- bot.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'bot.go') diff --git a/bot.go b/bot.go index 7c8ea75..8ddcee5 100644 --- a/bot.go +++ b/bot.go @@ -734,6 +734,8 @@ func cleanChatBody() { for i, msg := range chatBody.Messages { logger.Debug("cleanChatBody: before clean", "index", i, "role", msg.Role, "content_len", len(msg.Content), "has_content", msg.HasContent(), "tool_call_id", msg.ToolCallID) } + // TODO: consider case where we keep tool requests + // /completion msg where part meant for user and other part tool call chatBody.Messages = cleanToolCalls(chatBody.Messages) chatBody.Messages = cleanNullMessages(chatBody.Messages) logger.Debug("cleanChatBody: after cleaning", "original_len", originalLen, "new_len", len(chatBody.Messages)) @@ -925,7 +927,7 @@ func chatToTextSlice(showSys bool) []string { func chatToText(showSys bool) string { s := chatToTextSlice(showSys) - return strings.Join(s, "") + return strings.Join(s, "\n") } func removeThinking(chatBody *models.ChatBody) { -- cgit v1.2.3