diff options
| author | Grail Finder <wohilas@gmail.com> | 2025-12-19 11:06:22 +0300 |
|---|---|---|
| committer | Grail Finder <wohilas@gmail.com> | 2025-12-19 11:06:22 +0300 |
| commit | 67ea1aef0dafb9dc6f82e009cc1ecc613f71e520 (patch) | |
| tree | 331743d4edc223508e96055c0ec5753901273b57 | |
| parent | 5f852418d8d12868df83a9591b15e0846971fff9 (diff) | |
Feat: two agent types; WebAgentB impl
| -rw-r--r-- | agent/agent.go | 71 | ||||
| -rw-r--r-- | agent/format.go | 120 | ||||
| -rw-r--r-- | agent/request.go | 22 | ||||
| -rw-r--r-- | agent/webagent.go | 34 | ||||
| -rw-r--r-- | bot.go | 1 | ||||
| -rw-r--r-- | config/config.go | 2 | ||||
| -rw-r--r-- | props_table.go | 2 | ||||
| -rw-r--r-- | tui.go | 30 |
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 +} @@ -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) { @@ -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 |
