Refactor: moving tool related code into tools package

This commit is contained in:
Grail Finder
2026-03-15 08:05:12 +03:00
parent 619b19cb46
commit 1396b3eb05
9 changed files with 279 additions and 234 deletions

102
bot.go
View File

@@ -11,6 +11,7 @@ import (
"gf-lt/models" "gf-lt/models"
"gf-lt/rag" "gf-lt/rag"
"gf-lt/storage" "gf-lt/storage"
"gf-lt/tools"
"html" "html"
"io" "io"
"log/slog" "log/slog"
@@ -27,26 +28,38 @@ import (
) )
var ( var (
httpClient = &http.Client{} httpClient = &http.Client{}
cfg *config.Config cfg *config.Config
logger *slog.Logger logger *slog.Logger
logLevel = new(slog.LevelVar) logLevel = new(slog.LevelVar)
ctx, cancel = context.WithCancel(context.Background()) ctx, cancel = context.WithCancel(context.Background())
activeChatName string activeChatName string
chatRoundChan = make(chan *models.ChatRoundReq, 1) chatRoundChan = make(chan *models.ChatRoundReq, 1)
chunkChan = make(chan string, 10) chunkChan = make(chan string, 10)
openAIToolChan = make(chan string, 10) openAIToolChan = make(chan string, 10)
streamDone = make(chan bool, 1) streamDone = make(chan bool, 1)
chatBody *models.ChatBody chatBody *models.ChatBody
store storage.FullRepo store storage.FullRepo
defaultFirstMsg = "Hello! What can I do for you?" defaultStarter = []models.RoleMsg{}
defaultStarter = []models.RoleMsg{} interruptResp atomic.Bool
interruptResp atomic.Bool ragger *rag.RAG
ragger *rag.RAG chunkParser ChunkParser
chunkParser ChunkParser lastToolCall *models.FuncCall
lastToolCall *models.FuncCall lastRespStats *models.ResponseStats
lastRespStats *models.ResponseStats
//nolint:unused // TTS_ENABLED conditionally uses this //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 orator Orator
asr STT asr STT
localModelsMu sync.RWMutex 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. // monitorModelLoad starts a goroutine that periodically checks if the specified model is loaded.
func monitorModelLoad(modelID string) { func monitorModelLoad(modelID string) {
go func() { go func() {
@@ -1102,7 +1138,7 @@ func findCall(msg, toolCall string) bool {
// The ID should come from the streaming response (chunk.ToolID) set earlier. // 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. // Some tools like todo_create have "id" in their arguments which is NOT the tool call ID.
} else { } else {
jsStr := toolCallRE.FindString(msg) jsStr := tools.ToolCallRE.FindString(msg)
if jsStr == "" { // no tool call case if jsStr == "" { // no tool call case
return false return false
} }
@@ -1170,7 +1206,7 @@ func findCall(msg, toolCall string) bool {
Args: mapToString(lastToolCall.Args), Args: mapToString(lastToolCall.Args),
} }
// call a func // call a func
_, ok := fnMap[fc.Name] _, ok := tools.FnMap[fc.Name]
if !ok { if !ok {
m := fc.Name + " is not implemented" m := fc.Name + " is not implemented"
// Create tool response message with the proper tool_call_id // 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 // Show tool call progress indicator before execution
fmt.Fprintf(textView, "\n[yellow::i][tool: %s...][-:-:-]", fc.Name) fmt.Fprintf(textView, "\n[yellow::i][tool: %s...][-:-:-]", fc.Name)
toolRunningMode.Store(true) toolRunningMode.Store(true)
resp := callToolWithAgent(fc.Name, fc.Args) resp := tools.CallToolWithAgent(fc.Name, fc.Args)
toolRunningMode.Store(false) toolRunningMode.Store(false)
toolMsg := string(resp) toolMsg := string(resp)
logger.Info("llm used a tool call", "tool_name", fc.Name, "too_args", fc.Args, "id", fc.ID, "tool_resp", toolMsg) 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") text := strings.Join(s, "\n")
// Collapse thinking blocks if enabled // Collapse thinking blocks if enabled
if thinkingCollapsed { 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> // Extract content between <think> and </think>
start := len("<think>") start := len("<think>")
end := len(match) - len("</think>") end := len(match) - len("</think>")
@@ -1409,7 +1445,7 @@ func updateModelLists() {
chatBody.Model = m chatBody.Model = m
cachedModelColor.Store("green") cachedModelColor.Store("green")
updateStatusLine() updateStatusLine()
updateToolCapabilities() UpdateToolCapabilities()
app.Draw() app.Draw()
return return
} }
@@ -1441,7 +1477,7 @@ func summarizeAndStartNewChat() {
} }
showToast("info", "Summarizing chat history...") showToast("info", "Summarizing chat history...")
// Call the summarize_chat tool via agent // 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) summary := string(summaryBytes)
if summary == "" { if summary == "" {
showToast("error", "Failed to generate summary") showToast("error", "Failed to generate summary")
@@ -1477,8 +1513,8 @@ func init() {
return return
} }
defaultStarter = []models.RoleMsg{ defaultStarter = []models.RoleMsg{
{Role: "system", Content: basicSysMsg}, {Role: "system", Content: models.BasicSysMsg},
{Role: cfg.AssistantRole, Content: defaultFirstMsg}, {Role: cfg.AssistantRole, Content: models.DefaultFirstMsg},
} }
logfile, err := os.OpenFile(cfg.LogFile, logfile, err := os.OpenFile(cfg.LogFile,
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
@@ -1489,6 +1525,8 @@ func init() {
return return
} }
// load cards // load cards
sysMap[basicCard.ID] = basicCard
roleToID["assistant"] = basicCard.ID
basicCard.Role = cfg.AssistantRole basicCard.Role = cfg.AssistantRole
logLevel.Set(slog.LevelInfo) logLevel.Set(slog.LevelInfo)
logger = slog.New(slog.NewTextHandler(logfile, &slog.HandlerOptions{Level: logLevel})) logger = slog.New(slog.NewTextHandler(logfile, &slog.HandlerOptions{Level: logLevel}))
@@ -1530,15 +1568,14 @@ func init() {
} }
if cfg.PlaywrightEnabled { if cfg.PlaywrightEnabled {
go func() { go func() {
if err := checkPlaywright(); err != nil { if err := tools.CheckPlaywright(); err != nil {
// slow, need a faster check if playwright install if err := tools.InstallPW(); err != nil {
if err := installPW(); err != nil {
logger.Error("failed to install playwright", "error", err) logger.Error("failed to install playwright", "error", err)
cancel() cancel()
os.Exit(1) os.Exit(1)
return return
} }
if err := checkPlaywright(); err != nil { if err := tools.CheckPlaywright(); err != nil {
logger.Error("failed to run playwright", "error", err) logger.Error("failed to run playwright", "error", err)
cancel() cancel()
os.Exit(1) os.Exit(1)
@@ -1551,5 +1588,6 @@ func init() {
cachedModelColor.Store("orange") cachedModelColor.Store("orange")
go chatWatcher(ctx) go chatWatcher(ctx)
initTUI() initTUI()
initTools() tooler = tools.InitTools(cfg, logger, store)
tooler.RegisterWindowTools(modelHasVision)
} }

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"gf-lt/models" "gf-lt/models"
"gf-lt/pngmeta" "gf-lt/pngmeta"
"gf-lt/tools"
"image" "image"
"os" "os"
"os/exec" "os/exec"
@@ -86,8 +87,8 @@ func stripThinkingFromMsg(msg *models.RoleMsg) *models.RoleMsg {
} }
// Strip thinking from assistant messages // Strip thinking from assistant messages
msgText := msg.GetText() msgText := msg.GetText()
if thinkRE.MatchString(msgText) { if tools.ThinkRE.MatchString(msgText) {
cleanedText := thinkRE.ReplaceAllString(msgText, "") cleanedText := tools.ThinkRE.ReplaceAllString(msgText, "")
cleanedText = strings.TrimSpace(cleanedText) cleanedText = strings.TrimSpace(cleanedText)
msg.SetText(cleanedText) msg.SetText(cleanedText)
} }
@@ -148,7 +149,7 @@ func colorText() {
placeholderThink := "__THINK_BLOCK_%d__" placeholderThink := "__THINK_BLOCK_%d__"
counterThink := 0 counterThink := 0
// Replace code blocks with placeholders and store their styled versions // 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 // Style the code block and store it
styled := fmt.Sprintf("[red::i]%s[-:-:-]", match) styled := fmt.Sprintf("[red::i]%s[-:-:-]", match)
codeBlocks = append(codeBlocks, styled) codeBlocks = append(codeBlocks, styled)
@@ -157,7 +158,7 @@ func colorText() {
counter++ counter++
return id 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 // Style the code block and store it
styled := fmt.Sprintf("[red::i]%s[-:-:-]", match) styled := fmt.Sprintf("[red::i]%s[-:-:-]", match)
thinkBlocks = append(thinkBlocks, styled) thinkBlocks = append(thinkBlocks, styled)
@@ -167,10 +168,10 @@ func colorText() {
return id return id
}) })
// Step 2: Apply other regex styles to the non-code parts // Step 2: Apply other regex styles to the non-code parts
text = quotesRE.ReplaceAllString(text, `[orange::-]$1[-:-:-]`) text = tools.QuotesRE.ReplaceAllString(text, `[orange::-]$1[-:-:-]`)
text = starRE.ReplaceAllString(text, `[turquoise::i]$1[-:-:-]`) text = tools.StarRE.ReplaceAllString(text, `[turquoise::i]$1[-:-:-]`)
text = singleBacktickRE.ReplaceAllString(text, "`[pink::i]$1[-:-:-]`") text = tools.SingleBacktickRE.ReplaceAllString(text, "`[pink::i]$1[-:-:-]`")
// text = thinkRE.ReplaceAllString(text, `[yellow::i]$1[-:-:-]`) // text = tools.ThinkRE.ReplaceAllString(text, `[yellow::i]$1[-:-:-]`)
// Step 3: Restore the styled code blocks from placeholders // Step 3: Restore the styled code blocks from placeholders
for i, cb := range codeBlocks { for i, cb := range codeBlocks {
text = strings.Replace(text, fmt.Sprintf(placeholder, i), cb, 1) text = strings.Replace(text, fmt.Sprintf(placeholder, i), cb, 1)
@@ -188,7 +189,7 @@ func updateStatusLine() {
func initSysCards() ([]string, error) { func initSysCards() ([]string, error) {
labels := []string{} labels := []string{}
labels = append(labels, sysLabels...) labels = append(labels, tools.SysLabels...)
cards, err := pngmeta.ReadDirCards(cfg.SysDir, cfg.UserRole, logger) cards, err := pngmeta.ReadDirCards(cfg.SysDir, cfg.UserRole, logger)
if err != nil { if err != nil {
logger.Error("failed to read sys dir", "error", err) logger.Error("failed to read sys dir", "error", err)
@@ -1015,3 +1016,11 @@ func triggerPrivateMessageResponses(msg *models.RoleMsg) {
fmt.Fprint(textView, "[-:-:-]\n") fmt.Fprint(textView, "[-:-:-]\n")
chatRoundChan <- crr chatRoundChan <- crr
} }
func GetCardByRole(role string) *models.CharCard {
cardID, ok := roleToID[role]
if !ok {
return nil
}
return sysMap[cardID]
}

15
llm.go
View File

@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"gf-lt/models" "gf-lt/models"
"gf-lt/tools"
"io" "io"
"strings" "strings"
) )
@@ -11,10 +12,10 @@ import (
var imageAttachmentPath string // Global variable to track image attachment for next message var imageAttachmentPath string // Global variable to track image attachment for next message
var lastImg string // for ctrl+j 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 { func containsToolSysMsg() bool {
for i := range chatBody.Messages { 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 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 // sending description of the tools and how to use them
if cfg.ToolUse && !resume && role == cfg.UserRole && !containsToolSysMsg() { 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) filteredMessages, botPersona := filterMessagesForCurrentCharacter(chatBody.Messages)
// Build prompt and extract images inline as we process each message // 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, Tools: nil,
} }
if cfg.ToolUse && !resume && role != cfg.ToolRole { 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) data, err := json.Marshal(req)
if err != nil { 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 // sending description of the tools and how to use them
if cfg.ToolUse && !resume && role == cfg.UserRole && !containsToolSysMsg() { 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) filteredMessages, botPersona := filterMessagesForCurrentCharacter(chatBody.Messages)
messages := make([]string, len(filteredMessages)) 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 // sending description of the tools and how to use them
if cfg.ToolUse && !resume && role == cfg.UserRole && !containsToolSysMsg() { 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) filteredMessages, botPersona := filterMessagesForCurrentCharacter(chatBody.Messages)
messages := make([]string, len(filteredMessages)) 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) bodyCopy.Messages = consolidateAssistantMessages(bodyCopy.Messages)
orBody := models.NewOpenRouterChatReq(*bodyCopy, defaultLCPProps, cfg.ReasoningEffort) orBody := models.NewOpenRouterChatReq(*bodyCopy, defaultLCPProps, cfg.ReasoningEffort)
if cfg.ToolUse && !resume && role != cfg.ToolRole { 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) data, err := json.Marshal(orBody)
if err != nil { if err != nil {

View File

@@ -3,6 +3,8 @@ package models
const ( const (
LoadedMark = "(loaded) " LoadedMark = "(loaded) "
ToolRespMultyType = "multimodel_content" 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 type APIType int

View File

@@ -139,7 +139,7 @@ func showAPILinkSelectionPopup() {
apiListWidget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) { apiListWidget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
// Update the API in config // Update the API in config
cfg.CurrentAPI = mainText cfg.CurrentAPI = mainText
// updateToolCapabilities() // tools.UpdateToolCapabilities()
// Update model list based on new API // Update model list based on new API
// Helper function to get model list for a given API (same as in props_table.go) // Helper function to get model list for a given API (same as in props_table.go)
getModelListForAPI := func(api string) []string { getModelListForAPI := func(api string) []string {
@@ -159,7 +159,7 @@ func showAPILinkSelectionPopup() {
if len(newModelList) > 0 && !slices.Contains(newModelList, chatBody.Model) { if len(newModelList) > 0 && !slices.Contains(newModelList, chatBody.Model) {
chatBody.Model = strings.TrimPrefix(newModelList[0], models.LoadedMark) chatBody.Model = strings.TrimPrefix(newModelList[0], models.LoadedMark)
cfg.CurrentModel = chatBody.Model cfg.CurrentModel = chatBody.Model
updateToolCapabilities() UpdateToolCapabilities()
} }
pages.RemovePage("apiLinkSelectionPopup") pages.RemovePage("apiLinkSelectionPopup")
app.SetFocus(textArea) app.SetFocus(textArea)

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"gf-lt/tools"
"image" "image"
"os" "os"
"path" "path"
@@ -171,7 +172,7 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table {
return return
case "move sysprompt onto 1st msg": case "move sysprompt onto 1st msg":
chatBody.Messages[1].Content = chatBody.Messages[0].Content + chatBody.Messages[1].Content 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)) textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys))
activeChatName = selectedChat activeChatName = selectedChat
pages.RemovePage(historyPage) pages.RemovePage(historyPage)

View File

@@ -1,4 +1,4 @@
package main package tools
import ( import (
"encoding/json" "encoding/json"
@@ -101,7 +101,7 @@ var (
page playwright.Page page playwright.Page
) )
func pwShutDown() error { func PwShutDown() error {
if pw == nil { if pw == nil {
return nil return nil
} }
@@ -109,7 +109,7 @@ func pwShutDown() error {
return pw.Stop() return pw.Stop()
} }
func installPW() error { func InstallPW() error {
err := playwright.Install(&playwright.RunOptions{Verbose: false}) err := playwright.Install(&playwright.RunOptions{Verbose: false})
if err != nil { if err != nil {
logger.Warn("playwright not available", "error", err) logger.Warn("playwright not available", "error", err)
@@ -118,7 +118,7 @@ func installPW() error {
return nil return nil
} }
func checkPlaywright() error { func CheckPlaywright() error {
var err error var err error
pw, err = playwright.Run() pw, err = playwright.Run()
if err != nil { if err != nil {

View File

@@ -1,4 +1,4 @@
package main package tools
import ( import (
"context" "context"
@@ -8,8 +8,8 @@ import (
"gf-lt/config" "gf-lt/config"
"gf-lt/models" "gf-lt/models"
"gf-lt/storage" "gf-lt/storage"
"gf-lt/tools"
"io" "io"
"log/slog"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -25,20 +25,26 @@ import (
) )
var ( var (
toolCallRE = regexp.MustCompile(`__tool_call__\s*([\s\S]*?)__tool_call__`) ToolCallRE = regexp.MustCompile(`__tool_call__\s*([\s\S]*?)__tool_call__`)
quotesRE = regexp.MustCompile(`(".*?")`) QuotesRE = regexp.MustCompile(`(".*?")`)
starRE = regexp.MustCompile(`(\*.*?\*)`) StarRE = regexp.MustCompile(`(\*.*?\*)`)
thinkRE = regexp.MustCompile(`(<think>\s*([\s\S]*?)</think>)`) ThinkRE = regexp.MustCompile(`(?s)<think>.*?</think>`)
codeBlockRE = regexp.MustCompile(`(?s)\x60{3}(?:.*?)\n(.*?)\n\s*\x60{3}\s*`) toolCallRE = ToolCallRE
singleBacktickRE = regexp.MustCompile(`\x60([^\x60]*)\x60`) quotesRE = QuotesRE
roleRE = regexp.MustCompile(`^(\w+):`) starRE = StarRE
rpDefenitionSysMsg = ` 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. For this roleplay immersion is at most importance.
Every character thinks and acts based on their personality and setting of the roleplay. 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>. 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: Your current tools:
<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.` 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.` 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.` 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 webAgentClient *agent.AgentClient
webAgentClientOnce sync.Once webAgentClientOnce sync.Once
webAgentsOnce 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 WebSearcher searcher.WebSurfer
var ( var (
windowToolsAvailable bool xdotoolPath string
xdotoolPath string maimPath string
maimPath string logger *slog.Logger
modelHasVision bool cfg *config.Config
getTokenFunc func() string
) )
func initTools() { type Tools struct {
sysMap[basicCard.ID] = basicCard cfg *config.Config
roleToID["assistant"] = basicCard.ID 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 // Initialize fs root directory
tools.SetFSRoot(cfg.FilePickerDir) SetFSRoot(cfg.FilePickerDir)
// Initialize memory store // 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, "") sa, err := searcher.NewWebSurfer(searcher.SearcherTypeScraper, "")
if err != nil { if err != nil {
if logger != nil { if logger != nil {
@@ -174,88 +195,73 @@ func initTools() {
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)
} }
checkWindowTools() t := &Tools{
registerWindowTools() cfg: cfg,
} logger: logger,
store: store,
func GetCardByRole(role string) *models.CharCard {
cardID, ok := roleToID[role]
if !ok {
return nil
} }
return sysMap[cardID] t.checkWindowTools()
return t
} }
func checkWindowTools() { func (t *Tools) checkWindowTools() {
xdotoolPath, _ = exec.LookPath("xdotool") xdotoolPath, _ = exec.LookPath("xdotool")
maimPath, _ = exec.LookPath("maim") maimPath, _ = exec.LookPath("maim")
windowToolsAvailable = xdotoolPath != "" && maimPath != "" t.WindowToolsAvailable = xdotoolPath != "" && maimPath != ""
if windowToolsAvailable { if t.WindowToolsAvailable {
logger.Info("window tools available: xdotool and maim found") t.logger.Info("window tools available: xdotool and maim found")
} else { } else {
if xdotoolPath == "" { 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 == "" { 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() { func SetTokenFunc(fn func() string) {
if !cfg.ToolUse { getTokenFunc = fn
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
} }
// getWebAgentClient returns a singleton AgentClient for web agents.
func getWebAgentClient() *agent.AgentClient { func getWebAgentClient() *agent.AgentClient {
webAgentClientOnce.Do(func() { webAgentClientOnce.Do(func() {
getToken := func() string { getToken := func() string {
if chunkParser == nil { if getTokenFunc != nil {
return "" return getTokenFunc()
} }
return chunkParser.GetToken() return ""
} }
webAgentClient = agent.NewAgentClient(cfg, logger, getToken) webAgentClient = agent.NewAgentClient(cfg, logger, getToken)
}) })
return webAgentClient return webAgentClient
} }
// registerWebAgents registers WebAgentB instances for websearch and read_url tools. func RegisterWindowTools(modelHasVision bool) {
func registerWebAgents() { removeWindowToolsFromBaseTools()
webAgentsOnce.Do(func() { // Window tools registration happens here if needed
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 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) // 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
@@ -401,13 +407,13 @@ func readURLRaw(args map[string]string) []byte {
return []byte(fmt.Sprintf("%+v", resp)) return []byte(fmt.Sprintf("%+v", resp))
} }
// 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)
@@ -510,7 +516,7 @@ func runCmd(args map[string]string) []byte {
return []byte(getHelp(rest)) return []byte(getHelp(rest))
case "memory": case "memory":
// memory store <topic> <data> | memory get <topic> | memory list | memory forget <topic> // 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": case "todo":
// todo create|read|update|delete - route to existing todo handlers // todo create|read|update|delete - route to existing todo handlers
return []byte(handleTodoSubcommand(rest, args)) return []byte(handleTodoSubcommand(rest, args))
@@ -525,7 +531,7 @@ func runCmd(args map[string]string) []byte {
return captureWindowAndView(args) return captureWindowAndView(args)
case "view_img": case "view_img":
// view_img <file> - view image for multimodal // view_img <file> - view image for multimodal
return []byte(tools.FsViewImg(rest, "")) return []byte(FsViewImg(rest, ""))
case "browser": case "browser":
// browser <action> [args...] - Playwright browser automation // browser <action> [args...] - Playwright browser automation
return runBrowserCommand(rest, args) return runBrowserCommand(rest, args)
@@ -534,7 +540,7 @@ func runCmd(args map[string]string) []byte {
return executeCommand(args) return executeCommand(args)
case "git": case "git":
// git has its own whitelist in FsGit // git has its own whitelist in FsGit
return []byte(tools.FsGit(rest, "")) return []byte(FsGit(rest, ""))
default: default:
// Unknown subcommand - tell user to run help tool // Unknown subcommand - tell user to run help tool
return []byte("[error] command not allowed. Run 'help' tool to see available commands.") 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 // Use chain execution for pipe/chaining support
result := tools.ExecChain(commandStr) result := ExecChain(commandStr)
return []byte(result) return []byte(result)
} }
@@ -977,12 +983,10 @@ func handleCdCommand(args []string) []byte {
} else { } else {
targetDir = args[0] targetDir = args[0]
} }
// Resolve relative paths against current FilePickerDir // Resolve relative paths against current FilePickerDir
if !filepath.IsAbs(targetDir) { if !filepath.IsAbs(targetDir) {
targetDir = filepath.Join(cfg.FilePickerDir, targetDir) targetDir = filepath.Join(cfg.FilePickerDir, targetDir)
} }
// Verify the directory exists // Verify the directory exists
info, err := os.Stat(targetDir) info, err := os.Stat(targetDir)
if err != nil { if err != nil {
@@ -1188,7 +1192,7 @@ func viewImgTool(args map[string]string) []byte {
logger.Error(msg) logger.Error(msg)
return []byte(msg) return []byte(msg)
} }
result := tools.FsViewImg([]string{file}, "") result := FsViewImg([]string{file}, "")
return []byte(result) return []byte(result)
} }
@@ -1204,14 +1208,14 @@ func helpTool(args map[string]string) []byte {
return []byte(getHelp(rest)) return []byte(getHelp(rest))
} }
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.")
} // }
// Format chat history for the agent // // Format chat history for the agent
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)
} // }
func windowIDToHex(decimalID string) string { func windowIDToHex(decimalID string) string {
id, err := strconv.ParseInt(decimalID, 10, 64) id, err := strconv.ParseInt(decimalID, 10, 64)
@@ -1222,9 +1226,6 @@ func windowIDToHex(decimalID string) string {
} }
func listWindows(args map[string]string) []byte { 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", ".") cmd := exec.Command(xdotoolPath, "search", "--name", ".")
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
@@ -1257,9 +1258,6 @@ func listWindows(args map[string]string) []byte {
} }
func captureWindow(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"] window, ok := args["window"]
if !ok || window == "" { if !ok || window == "" {
return []byte("window parameter required (window ID or name)") 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 { func captureWindowAndView(args map[string]string) []byte {
if !windowToolsAvailable {
return []byte("window tools not available: xdotool or maim not found")
}
window, ok := args["window"] window, ok := args["window"]
if !ok || window == "" { if !ok || window == "" {
return []byte("window parameter required (window ID or name)") 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 { func cmdMemory(args map[string]string) []byte {
return []byte(tools.FsMemory(argsToSlice(args), "")) return []byte(FsMemory(argsToSlice(args), ""))
} }
type memoryAdapter struct { type memoryAdapter struct {
@@ -1400,7 +1395,7 @@ func (m *memoryAdapter) Forget(agent, topic string) error {
return m.store.Forget(agent, topic) return m.store.Forget(agent, topic)
} }
var fnMap = map[string]fnSig{ var FnMap = map[string]fnSig{
"memory": cmdMemory, "memory": cmdMemory,
"rag_search": ragsearch, "rag_search": ragsearch,
"websearch": websearch, "websearch": websearch,
@@ -1410,8 +1405,8 @@ var fnMap = map[string]fnSig{
"view_img": viewImgTool, "view_img": viewImgTool,
"help": helpTool, "help": helpTool,
// Unified run command // Unified run command
"run": runCmd, "run": runCmd,
"summarize_chat": summarizeChat, // "summarize_chat": summarizeChat,
} }
func removeWindowToolsFromBaseTools() { func removeWindowToolsFromBaseTools() {
@@ -1421,15 +1416,15 @@ func removeWindowToolsFromBaseTools() {
"capture_window_and_view": true, "capture_window_and_view": true,
} }
var filtered []models.Tool var filtered []models.Tool
for _, tool := range baseTools { for _, tool := range BaseTools {
if !windowToolNames[tool.Function.Name] { if !windowToolNames[tool.Function.Name] {
filtered = append(filtered, tool) filtered = append(filtered, tool)
} }
} }
baseTools = filtered BaseTools = filtered
delete(fnMap, "list_windows") delete(FnMap, "list_windows")
delete(fnMap, "capture_window") delete(FnMap, "capture_window")
delete(fnMap, "capture_window_and_view") delete(FnMap, "capture_window_and_view")
} }
func removePlaywrightToolsFromBaseTools() { func removePlaywrightToolsFromBaseTools() {
@@ -1448,31 +1443,31 @@ func removePlaywrightToolsFromBaseTools() {
"pw_drag": true, "pw_drag": true,
} }
var filtered []models.Tool var filtered []models.Tool
for _, tool := range baseTools { for _, tool := range BaseTools {
if !playwrightToolNames[tool.Function.Name] { if !playwrightToolNames[tool.Function.Name] {
filtered = append(filtered, tool) filtered = append(filtered, tool)
} }
} }
baseTools = filtered BaseTools = filtered
delete(fnMap, "pw_start") delete(FnMap, "pw_start")
delete(fnMap, "pw_stop") delete(FnMap, "pw_stop")
delete(fnMap, "pw_is_running") delete(FnMap, "pw_is_running")
delete(fnMap, "pw_navigate") delete(FnMap, "pw_navigate")
delete(fnMap, "pw_click") delete(FnMap, "pw_click")
delete(fnMap, "pw_click_at") delete(FnMap, "pw_click_at")
delete(fnMap, "pw_fill") delete(FnMap, "pw_fill")
delete(fnMap, "pw_extract_text") delete(FnMap, "pw_extract_text")
delete(fnMap, "pw_screenshot") delete(FnMap, "pw_screenshot")
delete(fnMap, "pw_screenshot_and_view") delete(FnMap, "pw_screenshot_and_view")
delete(fnMap, "pw_wait_for_selector") delete(FnMap, "pw_wait_for_selector")
delete(fnMap, "pw_drag") delete(FnMap, "pw_drag")
} }
func registerWindowTools() { func (t *Tools) RegisterWindowTools(modelHasVision bool) {
removeWindowToolsFromBaseTools() removeWindowToolsFromBaseTools()
if windowToolsAvailable { if t.WindowToolsAvailable {
fnMap["list_windows"] = listWindows FnMap["list_windows"] = listWindows
fnMap["capture_window"] = captureWindow FnMap["capture_window"] = captureWindow
windowTools := []models.Tool{ windowTools := []models.Tool{
{ {
Type: "function", Type: "function",
@@ -1505,7 +1500,7 @@ func registerWindowTools() {
}, },
} }
if modelHasVision { if modelHasVision {
fnMap["capture_window_and_view"] = captureWindowAndView FnMap["capture_window_and_view"] = captureWindowAndView
windowTools = append(windowTools, models.Tool{ windowTools = append(windowTools, models.Tool{
Type: "function", Type: "function",
Function: models.ToolFunc{ Function: models.ToolFunc{
@@ -1524,12 +1519,12 @@ func registerWindowTools() {
}, },
}) })
} }
baseTools = append(baseTools, windowTools...) BaseTools = append(BaseTools, windowTools...)
toolSysMsg += windowToolSysMsg 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. 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() { func registerPlaywrightTools() {
removePlaywrightToolsFromBaseTools() removePlaywrightToolsFromBaseTools()
if cfg != nil && cfg.PlaywrightEnabled { if cfg != nil && cfg.PlaywrightEnabled {
fnMap["pw_start"] = pwStart FnMap["pw_start"] = pwStart
fnMap["pw_stop"] = pwStop FnMap["pw_stop"] = pwStop
fnMap["pw_is_running"] = pwIsRunning FnMap["pw_is_running"] = pwIsRunning
fnMap["pw_navigate"] = pwNavigate FnMap["pw_navigate"] = pwNavigate
fnMap["pw_click"] = pwClick FnMap["pw_click"] = pwClick
fnMap["pw_click_at"] = pwClickAt FnMap["pw_click_at"] = pwClickAt
fnMap["pw_fill"] = pwFill FnMap["pw_fill"] = pwFill
fnMap["pw_extract_text"] = pwExtractText FnMap["pw_extract_text"] = pwExtractText
fnMap["pw_screenshot"] = pwScreenshot FnMap["pw_screenshot"] = pwScreenshot
fnMap["pw_screenshot_and_view"] = pwScreenshotAndView FnMap["pw_screenshot_and_view"] = pwScreenshotAndView
fnMap["pw_wait_for_selector"] = pwWaitForSelector FnMap["pw_wait_for_selector"] = pwWaitForSelector
fnMap["pw_drag"] = pwDrag FnMap["pw_drag"] = pwDrag
fnMap["pw_get_html"] = pwGetHTML FnMap["pw_get_html"] = pwGetHTML
fnMap["pw_get_dom"] = pwGetDOM FnMap["pw_get_dom"] = pwGetDOM
fnMap["pw_search_elements"] = pwSearchElements FnMap["pw_search_elements"] = pwSearchElements
playwrightTools := []models.Tool{ playwrightTools := []models.Tool{
{ {
Type: "function", Type: "function",
Function: models.ToolFunc{ Function: models.ToolFunc{
Name: "pw_start", 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{ Parameters: models.ToolFuncParams{
Type: "object", Type: "object",
Required: []string{}, Required: []string{},
@@ -1854,8 +1849,8 @@ func registerPlaywrightTools() {
}, },
}, },
} }
baseTools = append(baseTools, playwrightTools...) BaseTools = append(BaseTools, playwrightTools...)
toolSysMsg += browserToolSysMsg ToolSysMsg += browserToolSysMsg
agent.RegisterPWTool("pw_start", pwStart) agent.RegisterPWTool("pw_start", pwStart)
agent.RegisterPWTool("pw_stop", pwStop) agent.RegisterPWTool("pw_stop", pwStop)
agent.RegisterPWTool("pw_is_running", pwIsRunning) agent.RegisterPWTool("pw_is_running", pwIsRunning)
@@ -1876,7 +1871,7 @@ func registerPlaywrightTools() {
Type: "function", Type: "function",
Function: models.ToolFunc{ Function: models.ToolFunc{
Name: "browser_agent", 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{ Parameters: models.ToolFuncParams{
Type: "object", Type: "object",
Required: []string{"task"}, Required: []string{"task"},
@@ -1887,15 +1882,13 @@ func registerPlaywrightTools() {
}, },
}, },
} }
baseTools = append(baseTools, browserAgentTool...) BaseTools = append(BaseTools, browserAgentTool...)
fnMap["browser_agent"] = runBrowserAgent FnMap["browser_agent"] = runBrowserAgent
} }
} }
// 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 { f, ok := FnMap[name]
registerWebAgents()
f, ok := fnMap[name]
if !ok { if !ok {
return []byte(fmt.Sprintf("tool %s not found", name)) 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 // openai style def
var baseTools = []models.Tool{ var BaseTools = []models.Tool{
// rag_search // rag_search
models.Tool{ models.Tool{
Type: "function", Type: "function",

9
tui.go
View File

@@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"gf-lt/models" "gf-lt/models"
"gf-lt/tools"
"image" "image"
_ "image/jpeg" _ "image/jpeg"
_ "image/png" _ "image/png"
@@ -849,7 +850,7 @@ func initTUI() {
if event.Key() == tcell.KeyF9 { if event.Key() == tcell.KeyF9 {
// table of codeblocks to copy // table of codeblocks to copy
text := textView.GetText(false) text := textView.GetText(false)
cb := codeBlockRE.FindAllString(text, -1) cb := tools.CodeBlockRE.FindAllString(text, -1)
if len(cb) == 0 { if len(cb) == 0 {
showToast("notify", "no code blocks in chat") showToast("notify", "no code blocks in chat")
return nil return nil
@@ -948,7 +949,7 @@ func initTUI() {
if event.Key() == tcell.KeyCtrlK { if event.Key() == tcell.KeyCtrlK {
// add message from tools // add message from tools
cfg.ToolUse = !cfg.ToolUse cfg.ToolUse = !cfg.ToolUse
updateToolCapabilities() UpdateToolCapabilities()
updateStatusLine() updateStatusLine()
return nil return nil
} }
@@ -1054,7 +1055,7 @@ func initTUI() {
if event.Key() == tcell.KeyCtrlC { if event.Key() == tcell.KeyCtrlC {
logger.Info("caught Ctrl+C via tcell event") logger.Info("caught Ctrl+C via tcell event")
go func() { go func() {
if err := pwShutDown(); err != nil { if err := tools.PwShutDown(); err != nil {
logger.Error("shutdown failed", "err", err) logger.Error("shutdown failed", "err", err)
} }
app.Stop() app.Stop()
@@ -1146,7 +1147,7 @@ func initTUI() {
} }
// check if plain text // check if plain text
if !injectRole { if !injectRole {
matches := roleRE.FindStringSubmatch(msgText) matches := tools.RoleRE.FindStringSubmatch(msgText)
if len(matches) > 1 { if len(matches) > 1 {
persona = matches[1] persona = matches[1]
msgText = strings.TrimLeft(msgText[len(matches[0]):], " ") msgText = strings.TrimLeft(msgText[len(matches[0]):], " ")