diff options
| author | Grail Finder <wohilas@gmail.com> | 2026-03-15 08:05:12 +0300 |
|---|---|---|
| committer | Grail Finder <wohilas@gmail.com> | 2026-03-15 08:05:12 +0300 |
| commit | 1396b3eb05c32d868a3e07b8f60813f28d2042f8 (patch) | |
| tree | 85dede977e3a9adcaecefcf19397a9b416dfbc4c | |
| parent | 619b19cb46061c89aa4a837ed1e6bfea76644bd8 (diff) | |
Refactor: moving tool related code into tools package
| -rw-r--r-- | bot.go | 102 | ||||
| -rw-r--r-- | helpfuncs.go | 27 | ||||
| -rw-r--r-- | llm.go | 15 | ||||
| -rw-r--r-- | models/consts.go | 2 | ||||
| -rw-r--r-- | popups.go | 4 | ||||
| -rw-r--r-- | tables.go | 3 | ||||
| -rw-r--r-- | tools/pw.go (renamed from tools_playwright.go) | 8 | ||||
| -rw-r--r-- | tools/tools.go (renamed from tools.go) | 343 | ||||
| -rw-r--r-- | tui.go | 9 |
9 files changed, 279 insertions, 234 deletions
@@ -11,6 +11,7 @@ import ( "gf-lt/models" "gf-lt/rag" "gf-lt/storage" + "gf-lt/tools" "html" "io" "log/slog" @@ -27,26 +28,38 @@ import ( ) var ( - httpClient = &http.Client{} - cfg *config.Config - logger *slog.Logger - logLevel = new(slog.LevelVar) - ctx, cancel = context.WithCancel(context.Background()) - activeChatName string - chatRoundChan = make(chan *models.ChatRoundReq, 1) - chunkChan = make(chan string, 10) - openAIToolChan = make(chan string, 10) - streamDone = make(chan bool, 1) - chatBody *models.ChatBody - store storage.FullRepo - defaultFirstMsg = "Hello! What can I do for you?" - defaultStarter = []models.RoleMsg{} - interruptResp atomic.Bool - ragger *rag.RAG - chunkParser ChunkParser - lastToolCall *models.FuncCall - lastRespStats *models.ResponseStats + httpClient = &http.Client{} + cfg *config.Config + logger *slog.Logger + logLevel = new(slog.LevelVar) + ctx, cancel = context.WithCancel(context.Background()) + activeChatName string + chatRoundChan = make(chan *models.ChatRoundReq, 1) + chunkChan = make(chan string, 10) + openAIToolChan = make(chan string, 10) + streamDone = make(chan bool, 1) + chatBody *models.ChatBody + store storage.FullRepo + defaultStarter = []models.RoleMsg{} + interruptResp atomic.Bool + ragger *rag.RAG + chunkParser ChunkParser + lastToolCall *models.FuncCall + lastRespStats *models.ResponseStats //nolint:unused // TTS_ENABLED conditionally uses this + basicCard = &models.CharCard{ + ID: models.ComputeCardID("assistant", "basic_sys"), + SysPrompt: models.BasicSysMsg, + FirstMsg: models.DefaultFirstMsg, + Role: "assistant", + FilePath: "basic_sys", + } + sysMap = map[string]*models.CharCard{} + roleToID = map[string]string{} + modelHasVision bool + windowToolsAvailable bool + tooler *tools.Tools + // orator Orator asr STT localModelsMu sync.RWMutex @@ -458,6 +471,29 @@ func ModelHasVision(api, modelID string) bool { } } +func UpdateToolCapabilities() { + if !cfg.ToolUse { + return + } + modelHasVision = false + if cfg == nil || cfg.CurrentAPI == "" { + logger.Warn("cannot determine model capabilities: cfg or CurrentAPI is nil") + tooler.RegisterWindowTools(modelHasVision) + return + } + prevHasVision := modelHasVision + modelHasVision = ModelHasVision(cfg.CurrentAPI, cfg.CurrentModel) + if modelHasVision { + logger.Info("model has vision support", "model", cfg.CurrentModel, "api", cfg.CurrentAPI) + } else { + logger.Info("model does not have vision support", "model", cfg.CurrentModel, "api", cfg.CurrentAPI) + if windowToolsAvailable && !prevHasVision && !modelHasVision { + showToast("window tools", "Window capture-and-view unavailable: model lacks vision support") + } + } + tooler.RegisterWindowTools(modelHasVision) +} + // monitorModelLoad starts a goroutine that periodically checks if the specified model is loaded. func monitorModelLoad(modelID string) { go func() { @@ -1102,7 +1138,7 @@ func findCall(msg, toolCall string) bool { // The ID should come from the streaming response (chunk.ToolID) set earlier. // Some tools like todo_create have "id" in their arguments which is NOT the tool call ID. } else { - jsStr := toolCallRE.FindString(msg) + jsStr := tools.ToolCallRE.FindString(msg) if jsStr == "" { // no tool call case return false } @@ -1170,7 +1206,7 @@ func findCall(msg, toolCall string) bool { Args: mapToString(lastToolCall.Args), } // call a func - _, ok := fnMap[fc.Name] + _, ok := tools.FnMap[fc.Name] if !ok { m := fc.Name + " is not implemented" // Create tool response message with the proper tool_call_id @@ -1195,7 +1231,7 @@ func findCall(msg, toolCall string) bool { // Show tool call progress indicator before execution fmt.Fprintf(textView, "\n[yellow::i][tool: %s...][-:-:-]", fc.Name) toolRunningMode.Store(true) - resp := callToolWithAgent(fc.Name, fc.Args) + resp := tools.CallToolWithAgent(fc.Name, fc.Args) toolRunningMode.Store(false) toolMsg := string(resp) logger.Info("llm used a tool call", "tool_name", fc.Name, "too_args", fc.Args, "id", fc.ID, "tool_resp", toolMsg) @@ -1312,7 +1348,7 @@ func chatToText(messages []models.RoleMsg, showSys bool) string { text := strings.Join(s, "\n") // Collapse thinking blocks if enabled if thinkingCollapsed { - text = thinkRE.ReplaceAllStringFunc(text, func(match string) string { + text = tools.ThinkRE.ReplaceAllStringFunc(text, func(match string) string { // Extract content between <think> and </think> start := len("<think>") end := len(match) - len("</think>") @@ -1409,7 +1445,7 @@ func updateModelLists() { chatBody.Model = m cachedModelColor.Store("green") updateStatusLine() - updateToolCapabilities() + UpdateToolCapabilities() app.Draw() return } @@ -1441,7 +1477,7 @@ func summarizeAndStartNewChat() { } showToast("info", "Summarizing chat history...") // Call the summarize_chat tool via agent - summaryBytes := callToolWithAgent("summarize_chat", map[string]string{}) + summaryBytes := tools.CallToolWithAgent("summarize_chat", map[string]string{}) summary := string(summaryBytes) if summary == "" { showToast("error", "Failed to generate summary") @@ -1477,8 +1513,8 @@ func init() { return } defaultStarter = []models.RoleMsg{ - {Role: "system", Content: basicSysMsg}, - {Role: cfg.AssistantRole, Content: defaultFirstMsg}, + {Role: "system", Content: models.BasicSysMsg}, + {Role: cfg.AssistantRole, Content: models.DefaultFirstMsg}, } logfile, err := os.OpenFile(cfg.LogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) @@ -1489,6 +1525,8 @@ func init() { return } // load cards + sysMap[basicCard.ID] = basicCard + roleToID["assistant"] = basicCard.ID basicCard.Role = cfg.AssistantRole logLevel.Set(slog.LevelInfo) logger = slog.New(slog.NewTextHandler(logfile, &slog.HandlerOptions{Level: logLevel})) @@ -1530,15 +1568,14 @@ func init() { } if cfg.PlaywrightEnabled { go func() { - if err := checkPlaywright(); err != nil { - // slow, need a faster check if playwright install - if err := installPW(); err != nil { + if err := tools.CheckPlaywright(); err != nil { + if err := tools.InstallPW(); err != nil { logger.Error("failed to install playwright", "error", err) cancel() os.Exit(1) return } - if err := checkPlaywright(); err != nil { + if err := tools.CheckPlaywright(); err != nil { logger.Error("failed to run playwright", "error", err) cancel() os.Exit(1) @@ -1551,5 +1588,6 @@ func init() { cachedModelColor.Store("orange") go chatWatcher(ctx) initTUI() - initTools() + tooler = tools.InitTools(cfg, logger, store) + tooler.RegisterWindowTools(modelHasVision) } diff --git a/helpfuncs.go b/helpfuncs.go index e28beda..52866e2 100644 --- a/helpfuncs.go +++ b/helpfuncs.go @@ -4,6 +4,7 @@ import ( "fmt" "gf-lt/models" "gf-lt/pngmeta" + "gf-lt/tools" "image" "os" "os/exec" @@ -86,8 +87,8 @@ func stripThinkingFromMsg(msg *models.RoleMsg) *models.RoleMsg { } // Strip thinking from assistant messages msgText := msg.GetText() - if thinkRE.MatchString(msgText) { - cleanedText := thinkRE.ReplaceAllString(msgText, "") + if tools.ThinkRE.MatchString(msgText) { + cleanedText := tools.ThinkRE.ReplaceAllString(msgText, "") cleanedText = strings.TrimSpace(cleanedText) msg.SetText(cleanedText) } @@ -148,7 +149,7 @@ func colorText() { placeholderThink := "__THINK_BLOCK_%d__" counterThink := 0 // Replace code blocks with placeholders and store their styled versions - text = codeBlockRE.ReplaceAllStringFunc(text, func(match string) string { + text = tools.CodeBlockRE.ReplaceAllStringFunc(text, func(match string) string { // Style the code block and store it styled := fmt.Sprintf("[red::i]%s[-:-:-]", match) codeBlocks = append(codeBlocks, styled) @@ -157,7 +158,7 @@ func colorText() { counter++ return id }) - text = thinkRE.ReplaceAllStringFunc(text, func(match string) string { + text = tools.ThinkRE.ReplaceAllStringFunc(text, func(match string) string { // Style the code block and store it styled := fmt.Sprintf("[red::i]%s[-:-:-]", match) thinkBlocks = append(thinkBlocks, styled) @@ -167,10 +168,10 @@ func colorText() { return id }) // Step 2: Apply other regex styles to the non-code parts - text = quotesRE.ReplaceAllString(text, `[orange::-]$1[-:-:-]`) - text = starRE.ReplaceAllString(text, `[turquoise::i]$1[-:-:-]`) - text = singleBacktickRE.ReplaceAllString(text, "`[pink::i]$1[-:-:-]`") - // text = thinkRE.ReplaceAllString(text, `[yellow::i]$1[-:-:-]`) + text = tools.QuotesRE.ReplaceAllString(text, `[orange::-]$1[-:-:-]`) + text = tools.StarRE.ReplaceAllString(text, `[turquoise::i]$1[-:-:-]`) + text = tools.SingleBacktickRE.ReplaceAllString(text, "`[pink::i]$1[-:-:-]`") + // text = tools.ThinkRE.ReplaceAllString(text, `[yellow::i]$1[-:-:-]`) // Step 3: Restore the styled code blocks from placeholders for i, cb := range codeBlocks { text = strings.Replace(text, fmt.Sprintf(placeholder, i), cb, 1) @@ -188,7 +189,7 @@ func updateStatusLine() { func initSysCards() ([]string, error) { labels := []string{} - labels = append(labels, sysLabels...) + labels = append(labels, tools.SysLabels...) cards, err := pngmeta.ReadDirCards(cfg.SysDir, cfg.UserRole, logger) if err != nil { logger.Error("failed to read sys dir", "error", err) @@ -1015,3 +1016,11 @@ func triggerPrivateMessageResponses(msg *models.RoleMsg) { fmt.Fprint(textView, "[-:-:-]\n") chatRoundChan <- crr } + +func GetCardByRole(role string) *models.CharCard { + cardID, ok := roleToID[role] + if !ok { + return nil + } + return sysMap[cardID] +} @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "gf-lt/models" + "gf-lt/tools" "io" "strings" ) @@ -11,10 +12,10 @@ import ( var imageAttachmentPath string // Global variable to track image attachment for next message var lastImg string // for ctrl+j -// containsToolSysMsg checks if the toolSysMsg already exists in the chat body +// containsToolSysMsg checks if the tools.ToolSysMsg already exists in the chat body func containsToolSysMsg() bool { for i := range chatBody.Messages { - if chatBody.Messages[i].Role == cfg.ToolRole && chatBody.Messages[i].Content == toolSysMsg { + if chatBody.Messages[i].Role == cfg.ToolRole && chatBody.Messages[i].Content == tools.ToolSysMsg { return true } } @@ -144,7 +145,7 @@ func (lcp LCPCompletion) FormMsg(msg, role string, resume bool) (io.Reader, erro } // sending description of the tools and how to use them if cfg.ToolUse && !resume && role == cfg.UserRole && !containsToolSysMsg() { - chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg}) + chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: tools.ToolSysMsg}) } filteredMessages, botPersona := filterMessagesForCurrentCharacter(chatBody.Messages) // Build prompt and extract images inline as we process each message @@ -331,7 +332,7 @@ func (op LCPChat) FormMsg(msg, role string, resume bool) (io.Reader, error) { Tools: nil, } if cfg.ToolUse && !resume && role != cfg.ToolRole { - req.Tools = baseTools // set tools to use + req.Tools = tools.BaseTools // set tools to use } data, err := json.Marshal(req) if err != nil { @@ -384,7 +385,7 @@ func (ds DeepSeekerCompletion) FormMsg(msg, role string, resume bool) (io.Reader } // sending description of the tools and how to use them if cfg.ToolUse && !resume && role == cfg.UserRole && !containsToolSysMsg() { - chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg}) + chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: tools.ToolSysMsg}) } filteredMessages, botPersona := filterMessagesForCurrentCharacter(chatBody.Messages) messages := make([]string, len(filteredMessages)) @@ -536,7 +537,7 @@ func (or OpenRouterCompletion) FormMsg(msg, role string, resume bool) (io.Reader } // sending description of the tools and how to use them if cfg.ToolUse && !resume && role == cfg.UserRole && !containsToolSysMsg() { - chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg}) + chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: tools.ToolSysMsg}) } filteredMessages, botPersona := filterMessagesForCurrentCharacter(chatBody.Messages) messages := make([]string, len(filteredMessages)) @@ -671,7 +672,7 @@ func (or OpenRouterChat) FormMsg(msg, role string, resume bool) (io.Reader, erro bodyCopy.Messages = consolidateAssistantMessages(bodyCopy.Messages) orBody := models.NewOpenRouterChatReq(*bodyCopy, defaultLCPProps, cfg.ReasoningEffort) if cfg.ToolUse && !resume && role != cfg.ToolRole { - orBody.Tools = baseTools // set tools to use + orBody.Tools = tools.BaseTools // set tools to use } data, err := json.Marshal(orBody) if err != nil { diff --git a/models/consts.go b/models/consts.go index 8b4002b..14f1a49 100644 --- a/models/consts.go +++ b/models/consts.go @@ -3,6 +3,8 @@ package models const ( LoadedMark = "(loaded) " ToolRespMultyType = "multimodel_content" + DefaultFirstMsg = "Hello! What can I do for you?" + BasicSysMsg = "Large Language Model that helps user with any of his requests." ) type APIType int @@ -139,7 +139,7 @@ func showAPILinkSelectionPopup() { apiListWidget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) { // Update the API in config cfg.CurrentAPI = mainText - // updateToolCapabilities() + // tools.UpdateToolCapabilities() // Update model list based on new API // Helper function to get model list for a given API (same as in props_table.go) getModelListForAPI := func(api string) []string { @@ -159,7 +159,7 @@ func showAPILinkSelectionPopup() { if len(newModelList) > 0 && !slices.Contains(newModelList, chatBody.Model) { chatBody.Model = strings.TrimPrefix(newModelList[0], models.LoadedMark) cfg.CurrentModel = chatBody.Model - updateToolCapabilities() + UpdateToolCapabilities() } pages.RemovePage("apiLinkSelectionPopup") app.SetFocus(textArea) @@ -2,6 +2,7 @@ package main import ( "fmt" + "gf-lt/tools" "image" "os" "path" @@ -171,7 +172,7 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table { return case "move sysprompt onto 1st msg": chatBody.Messages[1].Content = chatBody.Messages[0].Content + chatBody.Messages[1].Content - chatBody.Messages[0].Content = rpDefenitionSysMsg + chatBody.Messages[0].Content = tools.RpDefenitionSysMsg textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys)) activeChatName = selectedChat pages.RemovePage(historyPage) diff --git a/tools_playwright.go b/tools/pw.go index 786b170..c21e8fe 100644 --- a/tools_playwright.go +++ b/tools/pw.go @@ -1,4 +1,4 @@ -package main +package tools import ( "encoding/json" @@ -101,7 +101,7 @@ var ( page playwright.Page ) -func pwShutDown() error { +func PwShutDown() error { if pw == nil { return nil } @@ -109,7 +109,7 @@ func pwShutDown() error { return pw.Stop() } -func installPW() error { +func InstallPW() error { err := playwright.Install(&playwright.RunOptions{Verbose: false}) if err != nil { logger.Warn("playwright not available", "error", err) @@ -118,7 +118,7 @@ func installPW() error { return nil } -func checkPlaywright() error { +func CheckPlaywright() error { var err error pw, err = playwright.Run() if err != nil { diff --git a/tools.go b/tools/tools.go index 3b6144f..ee20a2d 100644 --- a/tools.go +++ b/tools/tools.go @@ -1,4 +1,4 @@ -package main +package tools import ( "context" @@ -8,8 +8,8 @@ import ( "gf-lt/config" "gf-lt/models" "gf-lt/storage" - "gf-lt/tools" "io" + "log/slog" "os" "os/exec" "path/filepath" @@ -25,20 +25,26 @@ import ( ) var ( - toolCallRE = regexp.MustCompile(`__tool_call__\s*([\s\S]*?)__tool_call__`) - quotesRE = regexp.MustCompile(`(".*?")`) - starRE = regexp.MustCompile(`(\*.*?\*)`) - thinkRE = regexp.MustCompile(`(<think>\s*([\s\S]*?)</think>)`) - codeBlockRE = regexp.MustCompile(`(?s)\x60{3}(?:.*?)\n(.*?)\n\s*\x60{3}\s*`) - singleBacktickRE = regexp.MustCompile(`\x60([^\x60]*)\x60`) - roleRE = regexp.MustCompile(`^(\w+):`) - rpDefenitionSysMsg = ` + ToolCallRE = regexp.MustCompile(`__tool_call__\s*([\s\S]*?)__tool_call__`) + QuotesRE = regexp.MustCompile(`(".*?")`) + StarRE = regexp.MustCompile(`(\*.*?\*)`) + ThinkRE = regexp.MustCompile(`(?s)<think>.*?</think>`) + toolCallRE = ToolCallRE + quotesRE = QuotesRE + starRE = StarRE + thinkRE = ThinkRE + CodeBlockRE = regexp.MustCompile(`(?s)\x60{3}(?:.*?)\n(.*?)\n\s*\x60{3}\s*`) + SingleBacktickRE = regexp.MustCompile(`\x60([^\x60]*)\x60`) + codeBlockRE = CodeBlockRE + singleBacktickRE = SingleBacktickRE + RoleRE = regexp.MustCompile(`^(\w+):`) + SysLabels = []string{"assistant"} + RpDefenitionSysMsg = ` For this roleplay immersion is at most importance. Every character thinks and acts based on their personality and setting of the roleplay. Meta discussions outside of roleplay is allowed if clearly labeled as out of character, for example: (ooc: {msg}) or <ooc>{msg}</ooc>. ` - basicSysMsg = `Large Language Model that helps user with any of his requests.` - toolSysMsg = `You can do functions call if needed. + ToolSysMsg = `You can do functions call if needed. Your current tools: <tools> [ @@ -109,17 +115,6 @@ After that you are free to respond to the user. ragSearchSysPrompt = `Synthesize the document search results, extracting key information and presenting a concise answer. Provide sources and document IDs where relevant.` readURLSysPrompt = `Extract and summarize the content from the webpage. Provide key information, main points, and any relevant details.` summarySysPrompt = `Please provide a concise summary of the following conversation. Focus on key points, decisions, and actions. Provide only the summary, no additional commentary.` - basicCard = &models.CharCard{ - ID: models.ComputeCardID("assistant", "basic_sys"), - SysPrompt: basicSysMsg, - FirstMsg: defaultFirstMsg, - Role: "assistant", - FilePath: "basic_sys", - } - sysMap = map[string]*models.CharCard{} - roleToID = map[string]string{} - sysLabels = []string{"assistant"} - webAgentClient *agent.AgentClient webAgentClientOnce sync.Once webAgentsOnce sync.Once @@ -149,19 +144,45 @@ Additional window tools (available only if xdotool and maim are installed): var WebSearcher searcher.WebSurfer var ( - windowToolsAvailable bool - xdotoolPath string - maimPath string - modelHasVision bool + xdotoolPath string + maimPath string + logger *slog.Logger + cfg *config.Config + getTokenFunc func() string ) -func initTools() { - sysMap[basicCard.ID] = basicCard - roleToID["assistant"] = basicCard.ID +type Tools struct { + cfg *config.Config + logger *slog.Logger + store storage.FullRepo + WindowToolsAvailable bool + getTokenFunc func() string + webAgentClient *agent.AgentClient + webAgentClientOnce sync.Once +} + +func InitTools(cfg *config.Config, logger *slog.Logger, store storage.FullRepo) *Tools { + logger = logger + cfg = cfg + if cfg.PlaywrightEnabled { + if err := CheckPlaywright(); err != nil { + // slow, need a faster check if playwright install + if err := InstallPW(); err != nil { + logger.Error("failed to install playwright", "error", err) + os.Exit(1) + return nil + } + if err := CheckPlaywright(); err != nil { + logger.Error("failed to run playwright", "error", err) + os.Exit(1) + return nil + } + } + } // Initialize fs root directory - tools.SetFSRoot(cfg.FilePickerDir) + SetFSRoot(cfg.FilePickerDir) // Initialize memory store - tools.SetMemoryStore(&memoryAdapter{store: store, cfg: cfg}, cfg.AssistantRole) + SetMemoryStore(&memoryAdapter{store: store, cfg: cfg}, cfg.AssistantRole) sa, err := searcher.NewWebSurfer(searcher.SearcherTypeScraper, "") if err != nil { if logger != nil { @@ -174,88 +195,73 @@ func initTools() { if err := rag.Init(cfg, logger, store); err != nil { logger.Warn("failed to init rag; rag_search tool will not be available", "error", err) } - checkWindowTools() - registerWindowTools() -} - -func GetCardByRole(role string) *models.CharCard { - cardID, ok := roleToID[role] - if !ok { - return nil + t := &Tools{ + cfg: cfg, + logger: logger, + store: store, } - return sysMap[cardID] + t.checkWindowTools() + return t } -func checkWindowTools() { +func (t *Tools) checkWindowTools() { xdotoolPath, _ = exec.LookPath("xdotool") maimPath, _ = exec.LookPath("maim") - windowToolsAvailable = xdotoolPath != "" && maimPath != "" - if windowToolsAvailable { - logger.Info("window tools available: xdotool and maim found") + t.WindowToolsAvailable = xdotoolPath != "" && maimPath != "" + if t.WindowToolsAvailable { + t.logger.Info("window tools available: xdotool and maim found") } else { if xdotoolPath == "" { - logger.Warn("xdotool not found, window listing tools will not be available") + t.logger.Warn("xdotool not found, window listing tools will not be available") } if maimPath == "" { - logger.Warn("maim not found, window capture tools will not be available") + t.logger.Warn("maim not found, window capture tools will not be available") } } } -func updateToolCapabilities() { - if !cfg.ToolUse { - return - } - modelHasVision = false - if cfg == nil || cfg.CurrentAPI == "" { - logger.Warn("cannot determine model capabilities: cfg or CurrentAPI is nil") - registerWindowTools() - // fnMap["browser_agent"] = runBrowserAgent - return - } - prevHasVision := modelHasVision - modelHasVision = ModelHasVision(cfg.CurrentAPI, cfg.CurrentModel) - if modelHasVision { - logger.Info("model has vision support", "model", cfg.CurrentModel, "api", cfg.CurrentAPI) - } else { - logger.Info("model does not have vision support", "model", cfg.CurrentModel, "api", cfg.CurrentAPI) - if windowToolsAvailable && !prevHasVision && !modelHasVision { - showToast("window tools", "Window capture-and-view unavailable: model lacks vision support") - } - } - registerWindowTools() - // fnMap["browser_agent"] = runBrowserAgent +func SetTokenFunc(fn func() string) { + getTokenFunc = fn } -// getWebAgentClient returns a singleton AgentClient for web agents. func getWebAgentClient() *agent.AgentClient { webAgentClientOnce.Do(func() { getToken := func() string { - if chunkParser == nil { - return "" + if getTokenFunc != nil { + return getTokenFunc() } - return chunkParser.GetToken() + return "" } webAgentClient = agent.NewAgentClient(cfg, logger, getToken) }) return webAgentClient } -// registerWebAgents registers WebAgentB instances for websearch and read_url tools. -func registerWebAgents() { - webAgentsOnce.Do(func() { - client := getWebAgentClient() - // Register rag_search agent - agent.RegisterB("rag_search", agent.NewWebAgentB(client, ragSearchSysPrompt)) - // Register websearch agent - agent.RegisterB("websearch", agent.NewWebAgentB(client, webSearchSysPrompt)) - // Register read_url agent - agent.RegisterB("read_url", agent.NewWebAgentB(client, readURLSysPrompt)) - // Register summarize_chat agent - agent.RegisterB("summarize_chat", agent.NewWebAgentB(client, summarySysPrompt)) - }) +func RegisterWindowTools(modelHasVision bool) { + removeWindowToolsFromBaseTools() + // Window tools registration happens here if needed +} + +func RegisterPlaywrightTools() { + removePlaywrightToolsFromBaseTools() + if cfg != nil && cfg.PlaywrightEnabled { + // Playwright tools are registered here + } } +// webAgentsOnce.Do(func() { +// client := getWebAgentClient() +// // Register rag_search agent +// agent.RegisterB("rag_search", agent.NewWebAgentB(client, ragSearchSysPrompt)) +// // Register websearch agent +// agent.RegisterB("websearch", agent.NewWebAgentB(client, webSearchSysPrompt)) +// // Register read_url agent +// agent.RegisterB("read_url", agent.NewWebAgentB(client, readURLSysPrompt)) +// // Register summarize_chat agent +// agent.RegisterB("summarize_chat", agent.NewWebAgentB(client, summarySysPrompt)) +// }) +// } + // web search (depends on extra server) func websearch(args map[string]string) []byte { // make http request return bytes @@ -401,13 +407,13 @@ func readURLRaw(args map[string]string) []byte { return []byte(fmt.Sprintf("%+v", resp)) } -// Helper functions for file operations -func resolvePath(p string) string { - if filepath.IsAbs(p) { - return p - } - return filepath.Join(cfg.FilePickerDir, p) -} +// // Helper functions for file operations +// func resolvePath(p string) string { +// if filepath.IsAbs(p) { +// return p +// } +// return filepath.Join(cfg.FilePickerDir, p) +// } func readStringFromFile(filename string) (string, error) { data, err := os.ReadFile(filename) @@ -510,7 +516,7 @@ func runCmd(args map[string]string) []byte { return []byte(getHelp(rest)) case "memory": // memory store <topic> <data> | memory get <topic> | memory list | memory forget <topic> - return []byte(tools.FsMemory(append([]string{"store"}, rest...), "")) + return []byte(FsMemory(append([]string{"store"}, rest...), "")) case "todo": // todo create|read|update|delete - route to existing todo handlers return []byte(handleTodoSubcommand(rest, args)) @@ -525,7 +531,7 @@ func runCmd(args map[string]string) []byte { return captureWindowAndView(args) case "view_img": // view_img <file> - view image for multimodal - return []byte(tools.FsViewImg(rest, "")) + return []byte(FsViewImg(rest, "")) case "browser": // browser <action> [args...] - Playwright browser automation return runBrowserCommand(rest, args) @@ -534,7 +540,7 @@ func runCmd(args map[string]string) []byte { return executeCommand(args) case "git": // git has its own whitelist in FsGit - return []byte(tools.FsGit(rest, "")) + return []byte(FsGit(rest, "")) default: // Unknown subcommand - tell user to run help tool return []byte("[error] command not allowed. Run 'help' tool to see available commands.") @@ -958,7 +964,7 @@ func executeCommand(args map[string]string) []byte { } // Use chain execution for pipe/chaining support - result := tools.ExecChain(commandStr) + result := ExecChain(commandStr) return []byte(result) } @@ -977,12 +983,10 @@ func handleCdCommand(args []string) []byte { } else { targetDir = args[0] } - // Resolve relative paths against current FilePickerDir if !filepath.IsAbs(targetDir) { targetDir = filepath.Join(cfg.FilePickerDir, targetDir) } - // Verify the directory exists info, err := os.Stat(targetDir) if err != nil { @@ -1188,7 +1192,7 @@ func viewImgTool(args map[string]string) []byte { logger.Error(msg) return []byte(msg) } - result := tools.FsViewImg([]string{file}, "") + result := FsViewImg([]string{file}, "") return []byte(result) } @@ -1204,14 +1208,14 @@ func helpTool(args map[string]string) []byte { return []byte(getHelp(rest)) } -func summarizeChat(args map[string]string) []byte { - if len(chatBody.Messages) == 0 { - return []byte("No chat history to summarize.") - } - // Format chat history for the agent - chatText := chatToText(chatBody.Messages, true) // include system and tool messages - return []byte(chatText) -} +// func summarizeChat(args map[string]string) []byte { +// if len(chatBody.Messages) == 0 { +// return []byte("No chat history to summarize.") +// } +// // Format chat history for the agent +// chatText := chatToText(chatBody.Messages, true) // include system and tool messages +// return []byte(chatText) +// } func windowIDToHex(decimalID string) string { id, err := strconv.ParseInt(decimalID, 10, 64) @@ -1222,9 +1226,6 @@ func windowIDToHex(decimalID string) string { } func listWindows(args map[string]string) []byte { - if !windowToolsAvailable { - return []byte("window tools not available: xdotool or maim not found") - } cmd := exec.Command(xdotoolPath, "search", "--name", ".") output, err := cmd.Output() if err != nil { @@ -1257,9 +1258,6 @@ func listWindows(args map[string]string) []byte { } func captureWindow(args map[string]string) []byte { - if !windowToolsAvailable { - return []byte("window tools not available: xdotool or maim not found") - } window, ok := args["window"] if !ok || window == "" { return []byte("window parameter required (window ID or name)") @@ -1294,9 +1292,6 @@ func captureWindow(args map[string]string) []byte { } func captureWindowAndView(args map[string]string) []byte { - if !windowToolsAvailable { - return []byte("window tools not available: xdotool or maim not found") - } window, ok := args["window"] if !ok || window == "" { return []byte("window parameter required (window ID or name)") @@ -1365,7 +1360,7 @@ func argsToSlice(args map[string]string) []string { } func cmdMemory(args map[string]string) []byte { - return []byte(tools.FsMemory(argsToSlice(args), "")) + return []byte(FsMemory(argsToSlice(args), "")) } type memoryAdapter struct { @@ -1400,7 +1395,7 @@ func (m *memoryAdapter) Forget(agent, topic string) error { return m.store.Forget(agent, topic) } -var fnMap = map[string]fnSig{ +var FnMap = map[string]fnSig{ "memory": cmdMemory, "rag_search": ragsearch, "websearch": websearch, @@ -1410,8 +1405,8 @@ var fnMap = map[string]fnSig{ "view_img": viewImgTool, "help": helpTool, // Unified run command - "run": runCmd, - "summarize_chat": summarizeChat, + "run": runCmd, + // "summarize_chat": summarizeChat, } func removeWindowToolsFromBaseTools() { @@ -1421,15 +1416,15 @@ func removeWindowToolsFromBaseTools() { "capture_window_and_view": true, } var filtered []models.Tool - for _, tool := range baseTools { + for _, tool := range BaseTools { if !windowToolNames[tool.Function.Name] { filtered = append(filtered, tool) } } - baseTools = filtered - delete(fnMap, "list_windows") - delete(fnMap, "capture_window") - delete(fnMap, "capture_window_and_view") + BaseTools = filtered + delete(FnMap, "list_windows") + delete(FnMap, "capture_window") + delete(FnMap, "capture_window_and_view") } func removePlaywrightToolsFromBaseTools() { @@ -1448,31 +1443,31 @@ func removePlaywrightToolsFromBaseTools() { "pw_drag": true, } var filtered []models.Tool - for _, tool := range baseTools { + for _, tool := range BaseTools { if !playwrightToolNames[tool.Function.Name] { filtered = append(filtered, tool) } } - baseTools = filtered - delete(fnMap, "pw_start") - delete(fnMap, "pw_stop") - delete(fnMap, "pw_is_running") - delete(fnMap, "pw_navigate") - delete(fnMap, "pw_click") - delete(fnMap, "pw_click_at") - delete(fnMap, "pw_fill") - delete(fnMap, "pw_extract_text") - delete(fnMap, "pw_screenshot") - delete(fnMap, "pw_screenshot_and_view") - delete(fnMap, "pw_wait_for_selector") - delete(fnMap, "pw_drag") + BaseTools = filtered + delete(FnMap, "pw_start") + delete(FnMap, "pw_stop") + delete(FnMap, "pw_is_running") + delete(FnMap, "pw_navigate") + delete(FnMap, "pw_click") + delete(FnMap, "pw_click_at") + delete(FnMap, "pw_fill") + delete(FnMap, "pw_extract_text") + delete(FnMap, "pw_screenshot") + delete(FnMap, "pw_screenshot_and_view") + delete(FnMap, "pw_wait_for_selector") + delete(FnMap, "pw_drag") } -func registerWindowTools() { +func (t *Tools) RegisterWindowTools(modelHasVision bool) { removeWindowToolsFromBaseTools() - if windowToolsAvailable { - fnMap["list_windows"] = listWindows - fnMap["capture_window"] = captureWindow + if t.WindowToolsAvailable { + FnMap["list_windows"] = listWindows + FnMap["capture_window"] = captureWindow windowTools := []models.Tool{ { Type: "function", @@ -1505,7 +1500,7 @@ func registerWindowTools() { }, } if modelHasVision { - fnMap["capture_window_and_view"] = captureWindowAndView + FnMap["capture_window_and_view"] = captureWindowAndView windowTools = append(windowTools, models.Tool{ Type: "function", Function: models.ToolFunc{ @@ -1524,12 +1519,12 @@ func registerWindowTools() { }, }) } - baseTools = append(baseTools, windowTools...) - toolSysMsg += windowToolSysMsg + BaseTools = append(BaseTools, windowTools...) + ToolSysMsg += windowToolSysMsg } } -var browserAgentSysPrompt = `You are an autonomous browser automation agent. Your goal is to complete the user's task by intelligently using browser automation tools. +var browserAgentSysPrompt = `You are an autonomous browser automation agent. Your goal is to complete the user's task by intelligently using browser automation Important: The browser may already be running from a previous task! Always check pw_is_running first before starting a new browser. @@ -1574,27 +1569,27 @@ func runBrowserAgent(args map[string]string) []byte { func registerPlaywrightTools() { removePlaywrightToolsFromBaseTools() if cfg != nil && cfg.PlaywrightEnabled { - fnMap["pw_start"] = pwStart - fnMap["pw_stop"] = pwStop - fnMap["pw_is_running"] = pwIsRunning - fnMap["pw_navigate"] = pwNavigate - fnMap["pw_click"] = pwClick - fnMap["pw_click_at"] = pwClickAt - fnMap["pw_fill"] = pwFill - fnMap["pw_extract_text"] = pwExtractText - fnMap["pw_screenshot"] = pwScreenshot - fnMap["pw_screenshot_and_view"] = pwScreenshotAndView - fnMap["pw_wait_for_selector"] = pwWaitForSelector - fnMap["pw_drag"] = pwDrag - fnMap["pw_get_html"] = pwGetHTML - fnMap["pw_get_dom"] = pwGetDOM - fnMap["pw_search_elements"] = pwSearchElements + FnMap["pw_start"] = pwStart + FnMap["pw_stop"] = pwStop + FnMap["pw_is_running"] = pwIsRunning + FnMap["pw_navigate"] = pwNavigate + FnMap["pw_click"] = pwClick + FnMap["pw_click_at"] = pwClickAt + FnMap["pw_fill"] = pwFill + FnMap["pw_extract_text"] = pwExtractText + FnMap["pw_screenshot"] = pwScreenshot + FnMap["pw_screenshot_and_view"] = pwScreenshotAndView + FnMap["pw_wait_for_selector"] = pwWaitForSelector + FnMap["pw_drag"] = pwDrag + FnMap["pw_get_html"] = pwGetHTML + FnMap["pw_get_dom"] = pwGetDOM + FnMap["pw_search_elements"] = pwSearchElements playwrightTools := []models.Tool{ { Type: "function", Function: models.ToolFunc{ Name: "pw_start", - Description: "Start a Playwright browser instance. Call this first before using other pw_ tools. Uses headless mode by default (set PlaywrightHeadless=false in config for GUI).", + Description: "Start a Playwright browser instance. Call this first before using other pw_ Uses headless mode by default (set PlaywrightHeadless=false in config for GUI).", Parameters: models.ToolFuncParams{ Type: "object", Required: []string{}, @@ -1854,8 +1849,8 @@ func registerPlaywrightTools() { }, }, } - baseTools = append(baseTools, playwrightTools...) - toolSysMsg += browserToolSysMsg + BaseTools = append(BaseTools, playwrightTools...) + ToolSysMsg += browserToolSysMsg agent.RegisterPWTool("pw_start", pwStart) agent.RegisterPWTool("pw_stop", pwStop) agent.RegisterPWTool("pw_is_running", pwIsRunning) @@ -1876,7 +1871,7 @@ func registerPlaywrightTools() { Type: "function", Function: models.ToolFunc{ Name: "browser_agent", - Description: "Autonomous browser automation agent. Use for complex multi-step browser tasks like 'go to website, login, and take screenshot'. The agent will plan and execute steps automatically using browser tools.", + Description: "Autonomous browser automation agent. Use for complex multi-step browser tasks like 'go to website, login, and take screenshot'. The agent will plan and execute steps automatically using browser ", Parameters: models.ToolFuncParams{ Type: "object", Required: []string{"task"}, @@ -1887,15 +1882,13 @@ func registerPlaywrightTools() { }, }, } - baseTools = append(baseTools, browserAgentTool...) - fnMap["browser_agent"] = runBrowserAgent + BaseTools = append(BaseTools, browserAgentTool...) + FnMap["browser_agent"] = runBrowserAgent } } -// callToolWithAgent calls the tool and applies any registered agent. -func callToolWithAgent(name string, args map[string]string) []byte { - registerWebAgents() - f, ok := fnMap[name] +func CallToolWithAgent(name string, args map[string]string) []byte { + f, ok := FnMap[name] if !ok { return []byte(fmt.Sprintf("tool %s not found", name)) } @@ -1907,7 +1900,7 @@ func callToolWithAgent(name string, args map[string]string) []byte { } // openai style def -var baseTools = []models.Tool{ +var BaseTools = []models.Tool{ // rag_search models.Tool{ Type: "function", @@ -3,6 +3,7 @@ package main import ( "fmt" "gf-lt/models" + "gf-lt/tools" "image" _ "image/jpeg" _ "image/png" @@ -849,7 +850,7 @@ func initTUI() { if event.Key() == tcell.KeyF9 { // table of codeblocks to copy text := textView.GetText(false) - cb := codeBlockRE.FindAllString(text, -1) + cb := tools.CodeBlockRE.FindAllString(text, -1) if len(cb) == 0 { showToast("notify", "no code blocks in chat") return nil @@ -948,7 +949,7 @@ func initTUI() { if event.Key() == tcell.KeyCtrlK { // add message from tools cfg.ToolUse = !cfg.ToolUse - updateToolCapabilities() + UpdateToolCapabilities() updateStatusLine() return nil } @@ -1054,7 +1055,7 @@ func initTUI() { if event.Key() == tcell.KeyCtrlC { logger.Info("caught Ctrl+C via tcell event") go func() { - if err := pwShutDown(); err != nil { + if err := tools.PwShutDown(); err != nil { logger.Error("shutdown failed", "err", err) } app.Stop() @@ -1146,7 +1147,7 @@ func initTUI() { } // check if plain text if !injectRole { - matches := roleRE.FindStringSubmatch(msgText) + matches := tools.RoleRE.FindStringSubmatch(msgText) if len(matches) > 1 { persona = matches[1] msgText = strings.TrimLeft(msgText[len(matches[0]):], " ") |
