summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--agent/agent.go71
-rw-r--r--agent/format.go120
-rw-r--r--agent/request.go22
-rw-r--r--agent/webagent.go34
-rw-r--r--bot.go1
-rw-r--r--config/config.go2
-rw-r--r--props_table.go2
-rw-r--r--tui.go30
8 files changed, 99 insertions, 183 deletions
diff --git a/agent/agent.go b/agent/agent.go
index 30e30e3..5ad1ef1 100644
--- a/agent/agent.go
+++ b/agent/agent.go
@@ -1,60 +1,35 @@
package agent
-// Agent defines an interface for processing tool outputs.
-// An Agent can clean, summarize, or otherwise transform raw tool outputs
-// before they are presented to the main LLM.
-type Agent interface {
- // Process takes the original tool arguments and the raw output from the tool,
- // and returns a cleaned/summarized version suitable for the main LLM context.
- Process(args map[string]string, rawOutput []byte) []byte
-}
+// I see two types of agents possible:
+// ones who do their own tools calls
+// ones that works only with the output
-// registry holds mapping from tool names to agents.
-var registry = make(map[string]Agent)
+// A: main chat -> agent (handles everything: tool + processing)
+// B: main chat -> tool -> agent (process tool output)
-// Register adds an agent for a specific tool name.
-// If an agent already exists for the tool, it will be replaced.
-func Register(toolName string, a Agent) {
- registry[toolName] = a
+// AgenterA gets a task "find out weather in london"
+// proceeds to make tool calls on its own
+type AgenterA interface {
+ ProcessTask(task string) []byte
}
-// Get returns the agent for a tool name, or nil if none is registered.
-func Get(toolName string) Agent {
- return registry[toolName]
+// AgenterB defines an interface for processing tool outputs
+type AgenterB interface {
+ // Process takes the original tool arguments and the raw output from the tool,
+ // and returns a cleaned/summarized version suitable for the main LLM context
+ Process(args map[string]string, rawOutput []byte) []byte
}
-// FormatterAgent is a simple agent that applies formatting functions.
-type FormatterAgent struct {
- formatFunc func([]byte) (string, error)
-}
+// registry holds mapping from tool names to agents
+var RegistryB = make(map[string]AgenterB)
+var RegistryA = make(map[AgenterA][]string)
-// NewFormatterAgent creates a FormatterAgent that uses the given formatting function.
-func NewFormatterAgent(formatFunc func([]byte) (string, error)) *FormatterAgent {
- return &FormatterAgent{formatFunc: formatFunc}
+// Register adds an agent for a specific tool name
+// If an agent already exists for the tool, it will be replaced
+func RegisterB(toolName string, a AgenterB) {
+ RegistryB[toolName] = a
}
-// Process applies the formatting function to raw output.
-func (a *FormatterAgent) Process(args map[string]string, rawOutput []byte) []byte {
- if a.formatFunc == nil {
- return rawOutput
- }
- formatted, err := a.formatFunc(rawOutput)
- if err != nil {
- // On error, return raw output with a warning prefix
- return []byte("[formatting failed, showing raw output]\n" + string(rawOutput))
- }
- return []byte(formatted)
+func RegisterA(toolNames []string, a AgenterA) {
+ RegistryA[a] = toolNames
}
-
-// DefaultFormatter returns a FormatterAgent that uses the appropriate formatting
-// based on tool name.
-func DefaultFormatter(toolName string) Agent {
- switch toolName {
- case "websearch":
- return NewFormatterAgent(FormatSearchResults)
- case "read_url":
- return NewFormatterAgent(FormatWebPageContent)
- default:
- return nil
- }
-} \ No newline at end of file
diff --git a/agent/format.go b/agent/format.go
deleted file mode 100644
index 01ecb07..0000000
--- a/agent/format.go
+++ /dev/null
@@ -1,120 +0,0 @@
-package agent
-
-import (
- "encoding/json"
- "fmt"
- "strings"
-)
-
-// FormatSearchResults takes raw JSON from websearch and returns a concise summary.
-func FormatSearchResults(rawJSON []byte) (string, error) {
- // Try to unmarshal as generic slice of maps
- var results []map[string]interface{}
- if err := json.Unmarshal(rawJSON, &results); err != nil {
- // If that fails, try as a single map (maybe wrapper object)
- var wrapper map[string]interface{}
- if err2 := json.Unmarshal(rawJSON, &wrapper); err2 == nil {
- // Look for a "results" or "data" field
- if data, ok := wrapper["results"].([]interface{}); ok {
- // Convert to slice of maps
- for _, item := range data {
- if m, ok := item.(map[string]interface{}); ok {
- results = append(results, m)
- }
- }
- } else if data, ok := wrapper["data"].([]interface{}); ok {
- for _, item := range data {
- if m, ok := item.(map[string]interface{}); ok {
- results = append(results, m)
- }
- }
- } else {
- // No slice found, treat wrapper as single result
- results = []map[string]interface{}{wrapper}
- }
- } else {
- return "", fmt.Errorf("failed to unmarshal search results: %v (also %v)", err, err2)
- }
- }
-
- if len(results) == 0 {
- return "No search results found.", nil
- }
-
- var sb strings.Builder
- sb.WriteString(fmt.Sprintf("Found %d results:\n", len(results)))
- for i, r := range results {
- // Extract common fields
- title := getString(r, "title", "Title", "name", "heading")
- snippet := getString(r, "snippet", "description", "content", "body", "text", "summary")
- url := getString(r, "url", "link", "uri", "source")
-
- sb.WriteString(fmt.Sprintf("%d. ", i+1))
- if title != "" {
- sb.WriteString(fmt.Sprintf("**%s**", title))
- } else {
- sb.WriteString("(No title)")
- }
- if snippet != "" {
- // Truncate snippet to reasonable length
- if len(snippet) > 200 {
- snippet = snippet[:200] + "..."
- }
- sb.WriteString(fmt.Sprintf(" — %s", snippet))
- }
- if url != "" {
- sb.WriteString(fmt.Sprintf(" (%s)", url))
- }
- sb.WriteString("\n")
- }
- return sb.String(), nil
-}
-
-// FormatWebPageContent takes raw JSON from read_url and returns a concise summary.
-func FormatWebPageContent(rawJSON []byte) (string, error) {
- // Try to unmarshal as generic map
- var data map[string]interface{}
- if err := json.Unmarshal(rawJSON, &data); err != nil {
- // If that fails, try as string directly
- var content string
- if err2 := json.Unmarshal(rawJSON, &content); err2 == nil {
- return truncateText(content, 500), nil
- }
- // Both failed, return first error
- return "", fmt.Errorf("failed to unmarshal web page content: %v", err)
- }
-
- // Look for common content fields
- content := getString(data, "content", "text", "body", "article", "html", "markdown", "data")
- if content == "" {
- // If no content field, marshal the whole thing as a short string
- summary := fmt.Sprintf("%v", data)
- return truncateText(summary, 300), nil
- }
- return truncateText(content, 500), nil
-}
-
-// Helper to get a string value from a map, trying multiple keys.
-func getString(m map[string]interface{}, keys ...string) string {
- for _, k := range keys {
- if val, ok := m[k]; ok {
- switch v := val.(type) {
- case string:
- return v
- case fmt.Stringer:
- return v.String()
- default:
- return fmt.Sprintf("%v", v)
- }
- }
- }
- return ""
-}
-
-// Helper to truncate text and add ellipsis.
-func truncateText(s string, maxLen int) string {
- if len(s) <= maxLen {
- return s
- }
- return s[:maxLen] + "..."
-} \ No newline at end of file
diff --git a/agent/request.go b/agent/request.go
index 3b8d083..e10f03f 100644
--- a/agent/request.go
+++ b/agent/request.go
@@ -1,7 +1,10 @@
package agent
import (
+ "bytes"
+ "encoding/json"
"gf-lt/config"
+ "gf-lt/models"
"io"
"log/slog"
"net/http"
@@ -23,9 +26,28 @@ func NewAgentClient(cfg *config.Config, log slog.Logger, gt func() string) *Agen
}
}
+func (ag *AgentClient) FormMsg(sysprompt, msg string) (io.Reader, error) {
+ agentConvo := []models.RoleMsg{
+ {Role: "system", Content: sysprompt},
+ {Role: "user", Content: msg},
+ }
+ agentChat := &models.ChatBody{
+ Model: ag.cfg.CurrentModel,
+ Stream: true,
+ Messages: agentConvo,
+ }
+ b, err := json.Marshal(agentChat)
+ if err != nil {
+ ag.log.Error("failed to form agent msg", "error", err)
+ return nil, err
+ }
+ return bytes.NewReader(b), nil
+}
+
func (ag *AgentClient) LLMRequest(body io.Reader) ([]byte, error) {
req, err := http.NewRequest("POST", ag.cfg.CurrentAPI, body)
if err != nil {
+ ag.log.Error("llamacpp api", "error", err)
return nil, err
}
req.Header.Add("Accept", "application/json")
diff --git a/agent/webagent.go b/agent/webagent.go
new file mode 100644
index 0000000..0087e8e
--- /dev/null
+++ b/agent/webagent.go
@@ -0,0 +1,34 @@
+package agent
+
+import (
+ "fmt"
+ "log/slog"
+)
+
+// WebAgentB is a simple agent that applies formatting functions
+type WebAgentB struct {
+ *AgentClient
+ sysprompt string
+ log slog.Logger
+}
+
+// NewWebAgentB creates a WebAgentB that uses the given formatting function
+func NewWebAgentB(sysprompt string) *WebAgentB {
+ return &WebAgentB{sysprompt: sysprompt}
+}
+
+// Process applies the formatting function to raw output
+func (a *WebAgentB) Process(args map[string]string, rawOutput []byte) []byte {
+ msg, err := a.FormMsg(a.sysprompt,
+ fmt.Sprintf("request:\n%+v\ntool response:\n%v", args, string(rawOutput)))
+ if err != nil {
+ a.log.Error("failed to process the request", "error", err)
+ return []byte("failed to process the request; err: " + err.Error())
+ }
+ resp, err := a.LLMRequest(msg)
+ if err != nil {
+ a.log.Error("failed to process the request", "error", err)
+ return []byte("failed to process the request; err: " + err.Error())
+ }
+ return resp
+}
diff --git a/bot.go b/bot.go
index f2683bb..8206c63 100644
--- a/bot.go
+++ b/bot.go
@@ -263,6 +263,7 @@ func fetchLCPModelName() *models.LCPModels {
return nil
}
chatBody.Model = path.Base(llmModel.Data[0].ID)
+ cfg.CurrentModel = chatBody.Model
return &llmModel
}
diff --git a/config/config.go b/config/config.go
index eef8035..5b7cc35 100644
--- a/config/config.go
+++ b/config/config.go
@@ -12,7 +12,7 @@ type Config struct {
ChatAPI string `toml:"ChatAPI"`
CompletionAPI string `toml:"CompletionAPI"`
CurrentAPI string
- CurrentProvider string
+ CurrentModel string `toml:"CurrentModel"`
APIMap map[string]string
FetchModelNameAPI string `toml:"FetchModelNameAPI"`
// ToolsAPI list?
diff --git a/props_table.go b/props_table.go
index 774ea32..ae225d8 100644
--- a/props_table.go
+++ b/props_table.go
@@ -161,6 +161,7 @@ func makePropsTable(props map[string]float32) *tview.Table {
// Ensure chatBody.Model is in the new list; if not, set to first available model
if len(newModelList) > 0 && !slices.Contains(newModelList, chatBody.Model) {
chatBody.Model = newModelList[0]
+ cfg.CurrentModel = chatBody.Model
// Update the displayed cell text - need to find model row
// Search for model row by label
for r := 0; r < table.GetRowCount(); r++ {
@@ -179,6 +180,7 @@ func makePropsTable(props map[string]float32) *tview.Table {
modelList := getModelListForAPI(cfg.CurrentAPI)
addListPopupRow("Select a model", modelList, chatBody.Model, func(option string) {
chatBody.Model = option
+ cfg.CurrentModel = chatBody.Model
})
// Role selection dropdown
addListPopupRow("Write next message as", listRolesWithUser(), cfg.WriteNextMsgAs, func(option string) {
diff --git a/tui.go b/tui.go
index 53c8cfd..d3ce14e 100644
--- a/tui.go
+++ b/tui.go
@@ -18,21 +18,21 @@ import (
)
var (
- app *tview.Application
- pages *tview.Pages
- textArea *tview.TextArea
- editArea *tview.TextArea
- textView *tview.TextView
+ app *tview.Application
+ pages *tview.Pages
+ textArea *tview.TextArea
+ editArea *tview.TextArea
+ textView *tview.TextView
statusLineWidget *tview.TextView
- helpView *tview.TextView
- flex *tview.Flex
- imgView *tview.Image
- defaultImage = "sysprompts/llama.png"
- indexPickWindow *tview.InputField
- renameWindow *tview.InputField
- roleEditWindow *tview.InputField
- fullscreenMode bool
- positionVisible bool = true
+ helpView *tview.TextView
+ flex *tview.Flex
+ imgView *tview.Image
+ defaultImage = "sysprompts/llama.png"
+ indexPickWindow *tview.InputField
+ renameWindow *tview.InputField
+ roleEditWindow *tview.InputField
+ fullscreenMode bool
+ positionVisible bool = true
// pages
historyPage = "historyPage"
agentPage = "agentPage"
@@ -984,12 +984,14 @@ func init() {
if len(ORFreeModels) > 0 {
currentORModelIndex = (currentORModelIndex + 1) % len(ORFreeModels)
chatBody.Model = ORFreeModels[currentORModelIndex]
+ cfg.CurrentModel = chatBody.Model
}
updateStatusLine()
} else {
if len(LocalModels) > 0 {
currentLocalModelIndex = (currentLocalModelIndex + 1) % len(LocalModels)
chatBody.Model = LocalModels[currentLocalModelIndex]
+ cfg.CurrentModel = chatBody.Model
}
updateStatusLine()
// // For non-OpenRouter APIs, use the old logic