Refactor: moving tool related code into tools package
This commit is contained in:
102
bot.go
102
bot.go
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
27
helpfuncs.go
27
helpfuncs.go
@@ -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
15
llm.go
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -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
9
tui.go
@@ -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]):], " ")
|
||||||
|
|||||||
Reference in New Issue
Block a user