Chore: noblanks complaints

This commit is contained in:
Grail Finder
2026-02-25 21:02:58 +03:00
parent 888c9fec65
commit b97cd67d72
4 changed files with 1 additions and 153 deletions

View File

@@ -1,12 +1,10 @@
package main package main
import ( import (
"gf-lt/config" "gf-lt/config"
"gf-lt/models" "gf-lt/models"
"reflect" "reflect"
"testing" "testing"
) )
func TestConsolidateConsecutiveAssistantMessages(t *testing.T) { func TestConsolidateConsecutiveAssistantMessages(t *testing.T) {
// Mock config for testing // Mock config for testing
testCfg := &config.Config{ testCfg := &config.Config{
@@ -14,7 +12,6 @@ func TestConsolidateConsecutiveAssistantMessages(t *testing.T) {
WriteNextMsgAsCompletionAgent: "", WriteNextMsgAsCompletionAgent: "",
} }
cfg = testCfg cfg = testCfg
tests := []struct { tests := []struct {
name string name string
input []models.RoleMsg input []models.RoleMsg
@@ -114,38 +111,31 @@ func TestConsolidateConsecutiveAssistantMessages(t *testing.T) {
}, },
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result := consolidateAssistantMessages(tt.input) result := consolidateAssistantMessages(tt.input)
if len(result) != len(tt.expected) { if len(result) != len(tt.expected) {
t.Errorf("Expected %d messages, got %d", len(tt.expected), len(result)) t.Errorf("Expected %d messages, got %d", len(tt.expected), len(result))
t.Logf("Result: %+v", result) t.Logf("Result: %+v", result)
t.Logf("Expected: %+v", tt.expected) t.Logf("Expected: %+v", tt.expected)
return return
} }
for i, expectedMsg := range tt.expected { for i, expectedMsg := range tt.expected {
if i >= len(result) { if i >= len(result) {
t.Errorf("Result has fewer messages than expected at index %d", i) t.Errorf("Result has fewer messages than expected at index %d", i)
continue continue
} }
actualMsg := result[i] actualMsg := result[i]
if actualMsg.Role != expectedMsg.Role { if actualMsg.Role != expectedMsg.Role {
t.Errorf("Message %d: expected role '%s', got '%s'", i, expectedMsg.Role, actualMsg.Role) t.Errorf("Message %d: expected role '%s', got '%s'", i, expectedMsg.Role, actualMsg.Role)
} }
if actualMsg.Content != expectedMsg.Content { if actualMsg.Content != expectedMsg.Content {
t.Errorf("Message %d: expected content '%s', got '%s'", i, expectedMsg.Content, actualMsg.Content) t.Errorf("Message %d: expected content '%s', got '%s'", i, expectedMsg.Content, actualMsg.Content)
} }
if actualMsg.ToolCallID != expectedMsg.ToolCallID { if actualMsg.ToolCallID != expectedMsg.ToolCallID {
t.Errorf("Message %d: expected ToolCallID '%s', got '%s'", i, expectedMsg.ToolCallID, actualMsg.ToolCallID) t.Errorf("Message %d: expected ToolCallID '%s', got '%s'", i, expectedMsg.ToolCallID, actualMsg.ToolCallID)
} }
} }
// Additional check: ensure no messages were lost // Additional check: ensure no messages were lost
if !reflect.DeepEqual(result, tt.expected) { if !reflect.DeepEqual(result, tt.expected) {
t.Errorf("Result does not match expected:\nResult: %+v\nExpected: %+v", result, tt.expected) t.Errorf("Result does not match expected:\nResult: %+v\nExpected: %+v", result, tt.expected)
@@ -153,7 +143,6 @@ func TestConsolidateConsecutiveAssistantMessages(t *testing.T) {
}) })
} }
} }
func TestUnmarshalFuncCall(t *testing.T) { func TestUnmarshalFuncCall(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -213,7 +202,6 @@ func TestUnmarshalFuncCall(t *testing.T) {
wantErr: true, wantErr: true,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := unmarshalFuncCall(tt.jsonStr) got, err := unmarshalFuncCall(tt.jsonStr)
@@ -238,7 +226,6 @@ func TestUnmarshalFuncCall(t *testing.T) {
}) })
} }
} }
func TestConvertJSONToMapStringString(t *testing.T) { func TestConvertJSONToMapStringString(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -265,7 +252,6 @@ func TestConvertJSONToMapStringString(t *testing.T) {
wantErr: true, wantErr: true,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := convertJSONToMapStringString(tt.jsonStr) got, err := convertJSONToMapStringString(tt.jsonStr)
@@ -287,7 +273,6 @@ func TestConvertJSONToMapStringString(t *testing.T) {
}) })
} }
} }
func TestParseKnownToTag(t *testing.T) { func TestParseKnownToTag(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -378,7 +363,6 @@ func TestParseKnownToTag(t *testing.T) {
wantKnownTo: []string{"Alice", "Bob", "Carl"}, wantKnownTo: []string{"Alice", "Bob", "Carl"},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
// Set up config // Set up config
@@ -402,7 +386,6 @@ func TestParseKnownToTag(t *testing.T) {
}) })
} }
} }
func TestProcessMessageTag(t *testing.T) { func TestProcessMessageTag(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -498,7 +481,6 @@ func TestProcessMessageTag(t *testing.T) {
}, },
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
testCfg := &config.Config{ testCfg := &config.Config{
@@ -529,7 +511,6 @@ func TestProcessMessageTag(t *testing.T) {
}) })
} }
} }
func TestFilterMessagesForCharacter(t *testing.T) { func TestFilterMessagesForCharacter(t *testing.T) {
messages := []models.RoleMsg{ messages := []models.RoleMsg{
{Role: "system", Content: "System message", KnownTo: nil}, // visible to all {Role: "system", Content: "System message", KnownTo: nil}, // visible to all
@@ -539,7 +520,6 @@ func TestFilterMessagesForCharacter(t *testing.T) {
{Role: "Alice", Content: "Private to Carl", KnownTo: []string{"Alice", "Carl"}}, {Role: "Alice", Content: "Private to Carl", KnownTo: []string{"Alice", "Carl"}},
{Role: "Carl", Content: "Hi all", KnownTo: nil}, // visible to all {Role: "Carl", Content: "Hi all", KnownTo: nil}, // visible to all
} }
tests := []struct { tests := []struct {
name string name string
enabled bool enabled bool
@@ -583,7 +563,6 @@ func TestFilterMessagesForCharacter(t *testing.T) {
wantIndices: []int{0, 1, 5}, wantIndices: []int{0, 1, 5},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
testCfg := &config.Config{ testCfg := &config.Config{
@@ -591,15 +570,12 @@ func TestFilterMessagesForCharacter(t *testing.T) {
CharSpecificContextTag: "@", CharSpecificContextTag: "@",
} }
cfg = testCfg cfg = testCfg
got := filterMessagesForCharacter(messages, tt.character) got := filterMessagesForCharacter(messages, tt.character)
if len(got) != len(tt.wantIndices) { if len(got) != len(tt.wantIndices) {
t.Errorf("filterMessagesForCharacter() returned %d messages, want %d", len(got), len(tt.wantIndices)) t.Errorf("filterMessagesForCharacter() returned %d messages, want %d", len(got), len(tt.wantIndices))
t.Logf("got: %v", got) t.Logf("got: %v", got)
return return
} }
for i, idx := range tt.wantIndices { for i, idx := range tt.wantIndices {
if got[i].Content != messages[idx].Content { if got[i].Content != messages[idx].Content {
t.Errorf("filterMessagesForCharacter() message %d content = %q, want %q", i, got[i].Content, messages[idx].Content) t.Errorf("filterMessagesForCharacter() message %d content = %q, want %q", i, got[i].Content, messages[idx].Content)
@@ -608,7 +584,6 @@ func TestFilterMessagesForCharacter(t *testing.T) {
}) })
} }
} }
func TestRoleMsgCopyPreservesKnownTo(t *testing.T) { func TestRoleMsgCopyPreservesKnownTo(t *testing.T) {
// Test that the Copy() method preserves the KnownTo field // Test that the Copy() method preserves the KnownTo field
originalMsg := models.RoleMsg{ originalMsg := models.RoleMsg{
@@ -616,9 +591,7 @@ func TestRoleMsgCopyPreservesKnownTo(t *testing.T) {
Content: "Test message", Content: "Test message",
KnownTo: []string{"Bob", "Charlie"}, KnownTo: []string{"Bob", "Charlie"},
} }
copiedMsg := originalMsg.Copy() copiedMsg := originalMsg.Copy()
if copiedMsg.Role != originalMsg.Role { if copiedMsg.Role != originalMsg.Role {
t.Errorf("Copy() failed to preserve Role: got %q, want %q", copiedMsg.Role, originalMsg.Role) t.Errorf("Copy() failed to preserve Role: got %q, want %q", copiedMsg.Role, originalMsg.Role)
} }
@@ -635,7 +608,6 @@ func TestRoleMsgCopyPreservesKnownTo(t *testing.T) {
t.Errorf("Copy() failed to preserve hasContentParts flag") t.Errorf("Copy() failed to preserve hasContentParts flag")
} }
} }
func TestKnownToFieldPreservationScenario(t *testing.T) { func TestKnownToFieldPreservationScenario(t *testing.T) {
// Test the specific scenario from the log where KnownTo field was getting lost // Test the specific scenario from the log where KnownTo field was getting lost
originalMsg := models.RoleMsg{ originalMsg := models.RoleMsg{
@@ -643,28 +615,22 @@ func TestKnownToFieldPreservationScenario(t *testing.T) {
Content: `Alice: "Okay, Bob. The word is... **'Ephemeral'**. (ooc: @Bob@)"`, Content: `Alice: "Okay, Bob. The word is... **'Ephemeral'**. (ooc: @Bob@)"`,
KnownTo: []string{"Bob"}, // This was detected in the log KnownTo: []string{"Bob"}, // This was detected in the log
} }
t.Logf("Original message - Role: %s, Content: %s, KnownTo: %v", t.Logf("Original message - Role: %s, Content: %s, KnownTo: %v",
originalMsg.Role, originalMsg.Content, originalMsg.KnownTo) originalMsg.Role, originalMsg.Content, originalMsg.KnownTo)
// Simulate what happens when the message gets copied during processing // Simulate what happens when the message gets copied during processing
copiedMsg := originalMsg.Copy() copiedMsg := originalMsg.Copy()
t.Logf("Copied message - Role: %s, Content: %s, KnownTo: %v", t.Logf("Copied message - Role: %s, Content: %s, KnownTo: %v",
copiedMsg.Role, copiedMsg.Content, copiedMsg.KnownTo) copiedMsg.Role, copiedMsg.Content, copiedMsg.KnownTo)
// Check if KnownTo field survived the copy // Check if KnownTo field survived the copy
if len(copiedMsg.KnownTo) == 0 { if len(copiedMsg.KnownTo) == 0 {
t.Error("ERROR: KnownTo field was lost during copy!") t.Error("ERROR: KnownTo field was lost during copy!")
} else { } else {
t.Log("SUCCESS: KnownTo field was preserved during copy!") t.Log("SUCCESS: KnownTo field was preserved during copy!")
} }
// Verify the content is the same // Verify the content is the same
if copiedMsg.Content != originalMsg.Content { if copiedMsg.Content != originalMsg.Content {
t.Errorf("Content was changed during copy: got %s, want %s", copiedMsg.Content, originalMsg.Content) t.Errorf("Content was changed during copy: got %s, want %s", copiedMsg.Content, originalMsg.Content)
} }
// Verify the KnownTo slice is properly copied // Verify the KnownTo slice is properly copied
if !reflect.DeepEqual(copiedMsg.KnownTo, originalMsg.KnownTo) { if !reflect.DeepEqual(copiedMsg.KnownTo, originalMsg.KnownTo) {
t.Errorf("KnownTo was not properly copied: got %v, want %v", copiedMsg.KnownTo, originalMsg.KnownTo) t.Errorf("KnownTo was not properly copied: got %v, want %v", copiedMsg.KnownTo, originalMsg.KnownTo)

View File

@@ -1,10 +1,8 @@
package models package models
import ( import (
"strings" "strings"
"testing" "testing"
) )
func TestRoleMsgToTextWithImages(t *testing.T) { func TestRoleMsgToTextWithImages(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -92,7 +90,6 @@ func TestRoleMsgToTextWithImages(t *testing.T) {
expected: "[orange::i][image: /old/path/photo.jpg][-:-:-]", expected: "[orange::i][image: /old/path/photo.jpg][-:-:-]",
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result := tt.msg.ToText(tt.index) result := tt.msg.ToText(tt.index)
@@ -110,12 +107,10 @@ func TestRoleMsgToTextWithImages(t *testing.T) {
}) })
} }
} }
func TestExtractDisplayPath(t *testing.T) { func TestExtractDisplayPath(t *testing.T) {
// Save original base dir // Save original base dir
originalBaseDir := imageBaseDir originalBaseDir := imageBaseDir
defer func() { imageBaseDir = originalBaseDir }() defer func() { imageBaseDir = originalBaseDir }()
tests := []struct { tests := []struct {
name string name string
baseDir string baseDir string
@@ -153,7 +148,6 @@ func TestExtractDisplayPath(t *testing.T) {
expected: "..._that_exceeds_sixty_characters_limit_yes_it_is_very_long.jpg", expected: "..._that_exceeds_sixty_characters_limit_yes_it_is_very_long.jpg",
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
imageBaseDir = tt.baseDir imageBaseDir = tt.baseDir

View File

@@ -62,7 +62,6 @@ func TestORModelsListModels(t *testing.T) {
t.Errorf("expected 4 total models, got %d", len(allModels)) t.Errorf("expected 4 total models, got %d", len(allModels))
} }
}) })
t.Run("integration with or_models.json", func(t *testing.T) { t.Run("integration with or_models.json", func(t *testing.T) {
// Attempt to load the real data file from the project root // Attempt to load the real data file from the project root
path := filepath.Join("..", "or_models.json") path := filepath.Join("..", "or_models.json")

111
tools.go
View File

@@ -172,12 +172,10 @@ func init() {
panic("failed to init seachagent; error: " + err.Error()) panic("failed to init seachagent; error: " + err.Error())
} }
WebSearcher = sa WebSearcher = sa
if err := rag.Init(cfg, logger, store); err != nil { if err := rag.Init(cfg, logger, store); err != nil {
logger.Warn("failed to init rag; rag_search tool will not be available", "error", err) logger.Warn("failed to init rag; rag_search tool will not be available", "error", err)
} }
} }
// getWebAgentClient returns a singleton AgentClient for web agents. // getWebAgentClient returns a singleton AgentClient for web agents.
func getWebAgentClient() *agent.AgentClient { func getWebAgentClient() *agent.AgentClient {
webAgentClientOnce.Do(func() { webAgentClientOnce.Do(func() {
@@ -197,7 +195,6 @@ func getWebAgentClient() *agent.AgentClient {
}) })
return webAgentClient return webAgentClient
} }
// registerWebAgents registers WebAgentB instances for websearch and read_url tools. // registerWebAgents registers WebAgentB instances for websearch and read_url tools.
func registerWebAgents() { func registerWebAgents() {
webAgentsOnce.Do(func() { webAgentsOnce.Do(func() {
@@ -212,7 +209,6 @@ func registerWebAgents() {
agent.Register("summarize_chat", agent.NewWebAgentB(client, summarySysPrompt)) agent.Register("summarize_chat", agent.NewWebAgentB(client, summarySysPrompt))
}) })
} }
// web search (depends on extra server) // web search (depends on extra server)
func websearch(args map[string]string) []byte { func websearch(args map[string]string) []byte {
// make http request return bytes // make http request return bytes
@@ -246,7 +242,6 @@ func websearch(args map[string]string) []byte {
} }
return data return data
} }
// rag search (searches local document database) // rag search (searches local document database)
func ragsearch(args map[string]string) []byte { func ragsearch(args map[string]string) []byte {
query, ok := args["query"] query, ok := args["query"]
@@ -265,21 +260,18 @@ func ragsearch(args map[string]string) []byte {
"limit_arg", limitS, "error", err) "limit_arg", limitS, "error", err)
limit = 3 limit = 3
} }
ragInstance := rag.GetInstance() ragInstance := rag.GetInstance()
if ragInstance == nil { if ragInstance == nil {
msg := "rag not initialized; rag_search tool is not available" msg := "rag not initialized; rag_search tool is not available"
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
results, err := ragInstance.Search(query, limit) results, err := ragInstance.Search(query, limit)
if err != nil { if err != nil {
msg := "rag search failed; error: " + err.Error() msg := "rag search failed; error: " + err.Error()
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
data, err := json.Marshal(results) data, err := json.Marshal(results)
if err != nil { if err != nil {
msg := "failed to marshal rag search result; error: " + err.Error() msg := "failed to marshal rag search result; error: " + err.Error()
@@ -288,7 +280,6 @@ func ragsearch(args map[string]string) []byte {
} }
return data return data
} }
// web search raw (returns raw data without processing) // web search raw (returns raw data without processing)
func websearchRaw(args map[string]string) []byte { func websearchRaw(args map[string]string) []byte {
// make http request return bytes // make http request return bytes
@@ -317,7 +308,6 @@ func websearchRaw(args map[string]string) []byte {
// Return raw response without any processing // Return raw response without any processing
return []byte(fmt.Sprintf("%+v", resp)) return []byte(fmt.Sprintf("%+v", resp))
} }
// retrieves url content (text) // retrieves url content (text)
func readURL(args map[string]string) []byte { func readURL(args map[string]string) []byte {
// make http request return bytes // make http request return bytes
@@ -341,7 +331,6 @@ func readURL(args map[string]string) []byte {
} }
return data return data
} }
// retrieves url content raw (returns raw content without processing) // retrieves url content raw (returns raw content without processing)
func readURLRaw(args map[string]string) []byte { func readURLRaw(args map[string]string) []byte {
// make http request return bytes // make http request return bytes
@@ -360,7 +349,6 @@ func readURLRaw(args map[string]string) []byte {
// Return raw response without any processing // Return raw response without any processing
return []byte(fmt.Sprintf("%+v", resp)) return []byte(fmt.Sprintf("%+v", resp))
} }
/* /*
consider cases: consider cases:
- append mode (treat it like a journal appendix) - append mode (treat it like a journal appendix)
@@ -390,7 +378,6 @@ func memorise(args map[string]string) []byte {
msg := "info saved under the topic:" + args["topic"] msg := "info saved under the topic:" + args["topic"]
return []byte(msg) return []byte(msg)
} }
func recall(args map[string]string) []byte { func recall(args map[string]string) []byte {
agent := cfg.AssistantRole agent := cfg.AssistantRole
if len(args) < 1 { if len(args) < 1 {
@@ -406,7 +393,6 @@ func recall(args map[string]string) []byte {
answer := fmt.Sprintf("under the topic: %s is stored:\n%s", args["topic"], mind) answer := fmt.Sprintf("under the topic: %s is stored:\n%s", args["topic"], mind)
return []byte(answer) return []byte(answer)
} }
func recallTopics(args map[string]string) []byte { func recallTopics(args map[string]string) []byte {
agent := cfg.AssistantRole agent := cfg.AssistantRole
topics, err := store.RecallTopics(agent) topics, err := store.RecallTopics(agent)
@@ -417,9 +403,7 @@ func recallTopics(args map[string]string) []byte {
joinedS := strings.Join(topics, ";") joinedS := strings.Join(topics, ";")
return []byte(joinedS) return []byte(joinedS)
} }
// File Manipulation Tools // File Manipulation Tools
func fileCreate(args map[string]string) []byte { func fileCreate(args map[string]string) []byte {
path, ok := args["path"] path, ok := args["path"]
if !ok || path == "" { if !ok || path == "" {
@@ -427,24 +411,19 @@ func fileCreate(args map[string]string) []byte {
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
path = resolvePath(path) path = resolvePath(path)
content, ok := args["content"] content, ok := args["content"]
if !ok { if !ok {
content = "" content = ""
} }
if err := writeStringToFile(path, content); err != nil { if err := writeStringToFile(path, content); err != nil {
msg := "failed to create file; error: " + err.Error() msg := "failed to create file; error: " + err.Error()
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
msg := "file created successfully at " + path msg := "file created successfully at " + path
return []byte(msg) return []byte(msg)
} }
func fileRead(args map[string]string) []byte { func fileRead(args map[string]string) []byte {
path, ok := args["path"] path, ok := args["path"]
if !ok || path == "" { if !ok || path == "" {
@@ -452,16 +431,13 @@ func fileRead(args map[string]string) []byte {
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
path = resolvePath(path) path = resolvePath(path)
content, err := readStringFromFile(path) content, err := readStringFromFile(path)
if err != nil { if err != nil {
msg := "failed to read file; error: " + err.Error() msg := "failed to read file; error: " + err.Error()
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
result := map[string]string{ result := map[string]string{
"content": content, "content": content,
"path": path, "path": path,
@@ -472,10 +448,8 @@ func fileRead(args map[string]string) []byte {
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
return jsonResult return jsonResult
} }
func fileWrite(args map[string]string) []byte { func fileWrite(args map[string]string) []byte {
path, ok := args["path"] path, ok := args["path"]
if !ok || path == "" { if !ok || path == "" {
@@ -496,7 +470,6 @@ func fileWrite(args map[string]string) []byte {
msg := "file written successfully at " + path msg := "file written successfully at " + path
return []byte(msg) return []byte(msg)
} }
func fileWriteAppend(args map[string]string) []byte { func fileWriteAppend(args map[string]string) []byte {
path, ok := args["path"] path, ok := args["path"]
if !ok || path == "" { if !ok || path == "" {
@@ -517,7 +490,6 @@ func fileWriteAppend(args map[string]string) []byte {
msg := "file written successfully at " + path msg := "file written successfully at " + path
return []byte(msg) return []byte(msg)
} }
func fileDelete(args map[string]string) []byte { func fileDelete(args map[string]string) []byte {
path, ok := args["path"] path, ok := args["path"]
if !ok || path == "" { if !ok || path == "" {
@@ -525,19 +497,15 @@ func fileDelete(args map[string]string) []byte {
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
path = resolvePath(path) path = resolvePath(path)
if err := removeFile(path); err != nil { if err := removeFile(path); err != nil {
msg := "failed to delete file; error: " + err.Error() msg := "failed to delete file; error: " + err.Error()
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
msg := "file deleted successfully at " + path msg := "file deleted successfully at " + path
return []byte(msg) return []byte(msg)
} }
func fileMove(args map[string]string) []byte { func fileMove(args map[string]string) []byte {
src, ok := args["src"] src, ok := args["src"]
if !ok || src == "" { if !ok || src == "" {
@@ -546,7 +514,6 @@ func fileMove(args map[string]string) []byte {
return []byte(msg) return []byte(msg)
} }
src = resolvePath(src) src = resolvePath(src)
dst, ok := args["dst"] dst, ok := args["dst"]
if !ok || dst == "" { if !ok || dst == "" {
msg := "destination path not provided to file_move tool" msg := "destination path not provided to file_move tool"
@@ -554,17 +521,14 @@ func fileMove(args map[string]string) []byte {
return []byte(msg) return []byte(msg)
} }
dst = resolvePath(dst) dst = resolvePath(dst)
if err := moveFile(src, dst); err != nil { if err := moveFile(src, dst); err != nil {
msg := "failed to move file; error: " + err.Error() msg := "failed to move file; error: " + err.Error()
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
msg := fmt.Sprintf("file moved successfully from %s to %s", src, dst) msg := fmt.Sprintf("file moved successfully from %s to %s", src, dst)
return []byte(msg) return []byte(msg)
} }
func fileCopy(args map[string]string) []byte { func fileCopy(args map[string]string) []byte {
src, ok := args["src"] src, ok := args["src"]
if !ok || src == "" { if !ok || src == "" {
@@ -573,7 +537,6 @@ func fileCopy(args map[string]string) []byte {
return []byte(msg) return []byte(msg)
} }
src = resolvePath(src) src = resolvePath(src)
dst, ok := args["dst"] dst, ok := args["dst"]
if !ok || dst == "" { if !ok || dst == "" {
msg := "destination path not provided to file_copy tool" msg := "destination path not provided to file_copy tool"
@@ -581,32 +544,26 @@ func fileCopy(args map[string]string) []byte {
return []byte(msg) return []byte(msg)
} }
dst = resolvePath(dst) dst = resolvePath(dst)
if err := copyFile(src, dst); err != nil { if err := copyFile(src, dst); err != nil {
msg := "failed to copy file; error: " + err.Error() msg := "failed to copy file; error: " + err.Error()
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
msg := fmt.Sprintf("file copied successfully from %s to %s", src, dst) msg := fmt.Sprintf("file copied successfully from %s to %s", src, dst)
return []byte(msg) return []byte(msg)
} }
func fileList(args map[string]string) []byte { func fileList(args map[string]string) []byte {
path, ok := args["path"] path, ok := args["path"]
if !ok || path == "" { if !ok || path == "" {
path = "." // default to current directory path = "." // default to current directory
} }
path = resolvePath(path) path = resolvePath(path)
files, err := listDirectory(path) files, err := listDirectory(path)
if err != nil { if err != nil {
msg := "failed to list directory; error: " + err.Error() msg := "failed to list directory; error: " + err.Error()
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
result := map[string]interface{}{ result := map[string]interface{}{
"directory": path, "directory": path,
"files": files, "files": files,
@@ -617,19 +574,15 @@ func fileList(args map[string]string) []byte {
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
return jsonResult return jsonResult
} }
// Helper functions for file operations // Helper functions for file operations
func resolvePath(p string) string { func resolvePath(p string) string {
if filepath.IsAbs(p) { if filepath.IsAbs(p) {
return p return p
} }
return filepath.Join(cfg.FilePickerDir, p) return filepath.Join(cfg.FilePickerDir, p)
} }
func readStringFromFile(filename string) (string, error) { func readStringFromFile(filename string) (string, error) {
data, err := os.ReadFile(filename) data, err := os.ReadFile(filename)
if err != nil { if err != nil {
@@ -637,26 +590,21 @@ func readStringFromFile(filename string) (string, error) {
} }
return string(data), nil return string(data), nil
} }
func writeStringToFile(filename string, data string) error { func writeStringToFile(filename string, data string) error {
return os.WriteFile(filename, []byte(data), 0644) return os.WriteFile(filename, []byte(data), 0644)
} }
func appendStringToFile(filename string, data string) error { func appendStringToFile(filename string, data string) error {
file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil { if err != nil {
return err return err
} }
defer file.Close() defer file.Close()
_, err = file.WriteString(data) _, err = file.WriteString(data)
return err return err
} }
func removeFile(filename string) error { func removeFile(filename string) error {
return os.Remove(filename) return os.Remove(filename)
} }
func moveFile(src, dst string) error { func moveFile(src, dst string) error {
// First try with os.Rename (works within same filesystem) // First try with os.Rename (works within same filesystem)
if err := os.Rename(src, dst); err == nil { if err := os.Rename(src, dst); err == nil {
@@ -665,24 +613,20 @@ func moveFile(src, dst string) error {
// If that fails (e.g., cross-filesystem), copy and delete // If that fails (e.g., cross-filesystem), copy and delete
return copyAndRemove(src, dst) return copyAndRemove(src, dst)
} }
func copyFile(src, dst string) error { func copyFile(src, dst string) error {
srcFile, err := os.Open(src) srcFile, err := os.Open(src)
if err != nil { if err != nil {
return err return err
} }
defer srcFile.Close() defer srcFile.Close()
dstFile, err := os.Create(dst) dstFile, err := os.Create(dst)
if err != nil { if err != nil {
return err return err
} }
defer dstFile.Close() defer dstFile.Close()
_, err = io.Copy(dstFile, srcFile) _, err = io.Copy(dstFile, srcFile)
return err return err
} }
func copyAndRemove(src, dst string) error { func copyAndRemove(src, dst string) error {
// Copy the file // Copy the file
if err := copyFile(src, dst); err != nil { if err := copyFile(src, dst); err != nil {
@@ -691,13 +635,11 @@ func copyAndRemove(src, dst string) error {
// Remove the source file // Remove the source file
return os.Remove(src) return os.Remove(src)
} }
func listDirectory(path string) ([]string, error) { func listDirectory(path string) ([]string, error) {
entries, err := os.ReadDir(path) entries, err := os.ReadDir(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var files []string var files []string
for _, entry := range entries { for _, entry := range entries {
if entry.IsDir() { if entry.IsDir() {
@@ -706,12 +648,9 @@ func listDirectory(path string) ([]string, error) {
files = append(files, entry.Name()) files = append(files, entry.Name())
} }
} }
return files, nil return files, nil
} }
// Command Execution Tool // Command Execution Tool
func executeCommand(args map[string]string) []byte { func executeCommand(args map[string]string) []byte {
command, ok := args["command"] command, ok := args["command"]
if !ok || command == "" { if !ok || command == "" {
@@ -719,7 +658,6 @@ func executeCommand(args map[string]string) []byte {
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
// Get arguments - handle both single arg and multiple args // Get arguments - handle both single arg and multiple args
var cmdArgs []string var cmdArgs []string
if args["args"] != "" { if args["args"] != "" {
@@ -738,52 +676,42 @@ func executeCommand(args map[string]string) []byte {
argNum++ argNum++
} }
} }
if !isCommandAllowed(command, cmdArgs...) { if !isCommandAllowed(command, cmdArgs...) {
msg := fmt.Sprintf("command '%s' is not allowed", command) msg := fmt.Sprintf("command '%s' is not allowed", command)
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
// Execute with timeout for safety // Execute with timeout for safety
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, command, cmdArgs...) cmd := exec.CommandContext(ctx, command, cmdArgs...)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
msg := fmt.Sprintf("command '%s' failed; error: %v; output: %s", command, err, string(output)) msg := fmt.Sprintf("command '%s' failed; error: %v; output: %s", command, err, string(output))
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
// Check if output is empty and return success message // Check if output is empty and return success message
if len(output) == 0 { if len(output) == 0 {
successMsg := fmt.Sprintf("command '%s %s' executed successfully and exited with code 0", command, strings.Join(cmdArgs, " ")) successMsg := fmt.Sprintf("command '%s %s' executed successfully and exited with code 0", command, strings.Join(cmdArgs, " "))
return []byte(successMsg) return []byte(successMsg)
} }
return output return output
} }
// Helper functions for command execution // Helper functions for command execution
// Todo structure // Todo structure
type TodoItem struct { type TodoItem struct {
ID string `json:"id"` ID string `json:"id"`
Task string `json:"task"` Task string `json:"task"`
Status string `json:"status"` // "pending", "in_progress", "completed" Status string `json:"status"` // "pending", "in_progress", "completed"
} }
type TodoList struct { type TodoList struct {
Items []TodoItem `json:"items"` Items []TodoItem `json:"items"`
} }
// Global todo list storage // Global todo list storage
var globalTodoList = TodoList{ var globalTodoList = TodoList{
Items: []TodoItem{}, Items: []TodoItem{},
} }
// Todo Management Tools // Todo Management Tools
func todoCreate(args map[string]string) []byte { func todoCreate(args map[string]string) []byte {
task, ok := args["task"] task, ok := args["task"]
@@ -792,35 +720,28 @@ func todoCreate(args map[string]string) []byte {
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
// Generate simple ID // Generate simple ID
id := fmt.Sprintf("todo_%d", len(globalTodoList.Items)+1) id := fmt.Sprintf("todo_%d", len(globalTodoList.Items)+1)
newItem := TodoItem{ newItem := TodoItem{
ID: id, ID: id,
Task: task, Task: task,
Status: "pending", Status: "pending",
} }
globalTodoList.Items = append(globalTodoList.Items, newItem) globalTodoList.Items = append(globalTodoList.Items, newItem)
result := map[string]string{ result := map[string]string{
"message": "todo created successfully", "message": "todo created successfully",
"id": id, "id": id,
"task": task, "task": task,
"status": "pending", "status": "pending",
} }
jsonResult, err := json.Marshal(result) jsonResult, err := json.Marshal(result)
if err != nil { if err != nil {
msg := "failed to marshal result; error: " + err.Error() msg := "failed to marshal result; error: " + err.Error()
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
return jsonResult return jsonResult
} }
func todoRead(args map[string]string) []byte { func todoRead(args map[string]string) []byte {
id, ok := args["id"] id, ok := args["id"]
if ok && id != "" { if ok && id != "" {
@@ -851,7 +772,6 @@ func todoRead(args map[string]string) []byte {
} }
return jsonResult return jsonResult
} }
// Return all todos if no ID specified // Return all todos if no ID specified
result := map[string]interface{}{ result := map[string]interface{}{
"todos": globalTodoList.Items, "todos": globalTodoList.Items,
@@ -862,10 +782,8 @@ func todoRead(args map[string]string) []byte {
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
return jsonResult return jsonResult
} }
func todoUpdate(args map[string]string) []byte { func todoUpdate(args map[string]string) []byte {
id, ok := args["id"] id, ok := args["id"]
if !ok || id == "" { if !ok || id == "" {
@@ -873,16 +791,13 @@ func todoUpdate(args map[string]string) []byte {
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
task, taskOk := args["task"] task, taskOk := args["task"]
status, statusOk := args["status"] status, statusOk := args["status"]
if !taskOk && !statusOk { if !taskOk && !statusOk {
msg := "neither task nor status provided to todo_update tool" msg := "neither task nor status provided to todo_update tool"
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
// Find and update the todo // Find and update the todo
for i, item := range globalTodoList.Items { for i, item := range globalTodoList.Items {
if item.ID == id { if item.ID == id {
@@ -906,23 +821,19 @@ func todoUpdate(args map[string]string) []byte {
return jsonResult return jsonResult
} }
} }
result := map[string]string{ result := map[string]string{
"message": "todo updated successfully", "message": "todo updated successfully",
"id": id, "id": id,
} }
jsonResult, err := json.Marshal(result) jsonResult, err := json.Marshal(result)
if err != nil { if err != nil {
msg := "failed to marshal result; error: " + err.Error() msg := "failed to marshal result; error: " + err.Error()
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
return jsonResult return jsonResult
} }
} }
// ID not found // ID not found
result := map[string]string{ result := map[string]string{
"error": fmt.Sprintf("todo with id %s not found", id), "error": fmt.Sprintf("todo with id %s not found", id),
@@ -935,7 +846,6 @@ func todoUpdate(args map[string]string) []byte {
} }
return jsonResult return jsonResult
} }
func todoDelete(args map[string]string) []byte { func todoDelete(args map[string]string) []byte {
id, ok := args["id"] id, ok := args["id"]
if !ok || id == "" { if !ok || id == "" {
@@ -943,29 +853,24 @@ func todoDelete(args map[string]string) []byte {
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
// Find and remove the todo // Find and remove the todo
for i, item := range globalTodoList.Items { for i, item := range globalTodoList.Items {
if item.ID == id { if item.ID == id {
// Remove item from slice // Remove item from slice
globalTodoList.Items = append(globalTodoList.Items[:i], globalTodoList.Items[i+1:]...) globalTodoList.Items = append(globalTodoList.Items[:i], globalTodoList.Items[i+1:]...)
result := map[string]string{ result := map[string]string{
"message": "todo deleted successfully", "message": "todo deleted successfully",
"id": id, "id": id,
} }
jsonResult, err := json.Marshal(result) jsonResult, err := json.Marshal(result)
if err != nil { if err != nil {
msg := "failed to marshal result; error: " + err.Error() msg := "failed to marshal result; error: " + err.Error()
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
return jsonResult return jsonResult
} }
} }
// ID not found // ID not found
result := map[string]string{ result := map[string]string{
"error": fmt.Sprintf("todo with id %s not found", id), "error": fmt.Sprintf("todo with id %s not found", id),
@@ -978,7 +883,6 @@ func todoDelete(args map[string]string) []byte {
} }
return jsonResult return jsonResult
} }
var gitReadSubcommands = map[string]bool{ var gitReadSubcommands = map[string]bool{
"status": true, "status": true,
"log": true, "log": true,
@@ -990,7 +894,6 @@ var gitReadSubcommands = map[string]bool{
"shortlog": true, "shortlog": true,
"describe": true, "describe": true,
} }
func isCommandAllowed(command string, args ...string) bool { func isCommandAllowed(command string, args ...string) bool {
allowedCommands := map[string]bool{ allowedCommands := map[string]bool{
"grep": true, "grep": true,
@@ -1031,7 +934,6 @@ func isCommandAllowed(command string, args ...string) bool {
} }
return true return true
} }
func summarizeChat(args map[string]string) []byte { func summarizeChat(args map[string]string) []byte {
if len(chatBody.Messages) == 0 { if len(chatBody.Messages) == 0 {
return []byte("No chat history to summarize.") return []byte("No chat history to summarize.")
@@ -1040,9 +942,7 @@ func summarizeChat(args map[string]string) []byte {
chatText := chatToText(chatBody.Messages, true) // include system and tool messages chatText := chatToText(chatBody.Messages, true) // include system and tool messages
return []byte(chatText) return []byte(chatText)
} }
type fnSig func(map[string]string) []byte type fnSig func(map[string]string) []byte
var fnMap = map[string]fnSig{ var fnMap = map[string]fnSig{
"recall": recall, "recall": recall,
"recall_topics": recallTopics, "recall_topics": recallTopics,
@@ -1067,7 +967,6 @@ var fnMap = map[string]fnSig{
"todo_delete": todoDelete, "todo_delete": todoDelete,
"summarize_chat": summarizeChat, "summarize_chat": summarizeChat,
} }
// callToolWithAgent calls the tool and applies any registered agent. // callToolWithAgent calls the tool and applies any registered agent.
func callToolWithAgent(name string, args map[string]string) []byte { func callToolWithAgent(name string, args map[string]string) []byte {
registerWebAgents() registerWebAgents()
@@ -1081,7 +980,6 @@ func callToolWithAgent(name string, args map[string]string) []byte {
} }
return raw return raw
} }
// openai style def // openai style def
var baseTools = []models.Tool{ var baseTools = []models.Tool{
// rag_search // rag_search
@@ -1239,7 +1137,6 @@ var baseTools = []models.Tool{
}, },
}, },
}, },
// file_create // file_create
models.Tool{ models.Tool{
Type: "function", Type: "function",
@@ -1262,7 +1159,6 @@ var baseTools = []models.Tool{
}, },
}, },
}, },
// file_read // file_read
models.Tool{ models.Tool{
Type: "function", Type: "function",
@@ -1281,7 +1177,6 @@ var baseTools = []models.Tool{
}, },
}, },
}, },
// file_write // file_write
models.Tool{ models.Tool{
Type: "function", Type: "function",
@@ -1304,7 +1199,6 @@ var baseTools = []models.Tool{
}, },
}, },
}, },
// file_write_append // file_write_append
models.Tool{ models.Tool{
Type: "function", Type: "function",
@@ -1327,7 +1221,6 @@ var baseTools = []models.Tool{
}, },
}, },
}, },
// file_delete // file_delete
models.Tool{ models.Tool{
Type: "function", Type: "function",
@@ -1346,7 +1239,6 @@ var baseTools = []models.Tool{
}, },
}, },
}, },
// file_move // file_move
models.Tool{ models.Tool{
Type: "function", Type: "function",
@@ -1369,7 +1261,6 @@ var baseTools = []models.Tool{
}, },
}, },
}, },
// file_copy // file_copy
models.Tool{ models.Tool{
Type: "function", Type: "function",
@@ -1392,7 +1283,6 @@ var baseTools = []models.Tool{
}, },
}, },
}, },
// file_list // file_list
models.Tool{ models.Tool{
Type: "function", Type: "function",
@@ -1411,7 +1301,6 @@ var baseTools = []models.Tool{
}, },
}, },
}, },
// execute_command // execute_command
models.Tool{ models.Tool{
Type: "function", Type: "function",