summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGrail Finder <wohilas@gmail.com>2026-03-15 08:05:12 +0300
committerGrail Finder <wohilas@gmail.com>2026-03-15 08:05:12 +0300
commit1396b3eb05c32d868a3e07b8f60813f28d2042f8 (patch)
tree85dede977e3a9adcaecefcf19397a9b416dfbc4c
parent619b19cb46061c89aa4a837ed1e6bfea76644bd8 (diff)
Refactor: moving tool related code into tools package
-rw-r--r--bot.go102
-rw-r--r--helpfuncs.go27
-rw-r--r--llm.go15
-rw-r--r--models/consts.go2
-rw-r--r--popups.go4
-rw-r--r--tables.go3
-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.go9
9 files changed, 279 insertions, 234 deletions
diff --git a/bot.go b/bot.go
index a639cc9..4dc6895 100644
--- a/bot.go
+++ b/bot.go
@@ -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]
+}
diff --git a/llm.go b/llm.go
index 0e77bc9..cf56a45 100644
--- a/llm.go
+++ b/llm.go
@@ -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
diff --git a/popups.go b/popups.go
index 38f42cd..fe43666 100644
--- a/popups.go
+++ b/popups.go
@@ -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)
diff --git a/tables.go b/tables.go
index e47a1ce..d175ae6 100644
--- a/tables.go
+++ b/tables.go
@@ -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",
diff --git a/tui.go b/tui.go
index b8674c4..24074a6 100644
--- a/tui.go
+++ b/tui.go
@@ -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]):], " ")