Feat: two agent types; WebAgentB impl
This commit is contained in:
@@ -1,60 +1,35 @@
|
|||||||
package agent
|
package agent
|
||||||
|
|
||||||
// Agent defines an interface for processing tool outputs.
|
// I see two types of agents possible:
|
||||||
// An Agent can clean, summarize, or otherwise transform raw tool outputs
|
// ones who do their own tools calls
|
||||||
// before they are presented to the main LLM.
|
// ones that works only with the output
|
||||||
type Agent interface {
|
|
||||||
|
// A: main chat -> agent (handles everything: tool + processing)
|
||||||
|
// B: main chat -> tool -> agent (process tool output)
|
||||||
|
|
||||||
|
// AgenterA gets a task "find out weather in london"
|
||||||
|
// proceeds to make tool calls on its own
|
||||||
|
type AgenterA interface {
|
||||||
|
ProcessTask(task string) []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgenterB defines an interface for processing tool outputs
|
||||||
|
type AgenterB interface {
|
||||||
// Process takes the original tool arguments and the raw output from the tool,
|
// 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.
|
// and returns a cleaned/summarized version suitable for the main LLM context
|
||||||
Process(args map[string]string, rawOutput []byte) []byte
|
Process(args map[string]string, rawOutput []byte) []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// registry holds mapping from tool names to agents.
|
// registry holds mapping from tool names to agents
|
||||||
var registry = make(map[string]Agent)
|
var RegistryB = make(map[string]AgenterB)
|
||||||
|
var RegistryA = make(map[AgenterA][]string)
|
||||||
|
|
||||||
// Register adds an agent for a specific tool name.
|
// Register adds an agent for a specific tool name
|
||||||
// If an agent already exists for the tool, it will be replaced.
|
// If an agent already exists for the tool, it will be replaced
|
||||||
func Register(toolName string, a Agent) {
|
func RegisterB(toolName string, a AgenterB) {
|
||||||
registry[toolName] = a
|
RegistryB[toolName] = a
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the agent for a tool name, or nil if none is registered.
|
func RegisterA(toolNames []string, a AgenterA) {
|
||||||
func Get(toolName string) Agent {
|
RegistryA[a] = toolNames
|
||||||
return registry[toolName]
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormatterAgent is a simple agent that applies formatting functions.
|
|
||||||
type FormatterAgent struct {
|
|
||||||
formatFunc func([]byte) (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFormatterAgent creates a FormatterAgent that uses the given formatting function.
|
|
||||||
func NewFormatterAgent(formatFunc func([]byte) (string, error)) *FormatterAgent {
|
|
||||||
return &FormatterAgent{formatFunc: formatFunc}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
120
agent/format.go
120
agent/format.go
@@ -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] + "..."
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"gf-lt/config"
|
"gf-lt/config"
|
||||||
|
"gf-lt/models"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"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) {
|
func (ag *AgentClient) LLMRequest(body io.Reader) ([]byte, error) {
|
||||||
req, err := http.NewRequest("POST", ag.cfg.CurrentAPI, body)
|
req, err := http.NewRequest("POST", ag.cfg.CurrentAPI, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
ag.log.Error("llamacpp api", "error", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header.Add("Accept", "application/json")
|
req.Header.Add("Accept", "application/json")
|
||||||
|
|||||||
34
agent/webagent.go
Normal file
34
agent/webagent.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
1
bot.go
1
bot.go
@@ -263,6 +263,7 @@ func fetchLCPModelName() *models.LCPModels {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
chatBody.Model = path.Base(llmModel.Data[0].ID)
|
chatBody.Model = path.Base(llmModel.Data[0].ID)
|
||||||
|
cfg.CurrentModel = chatBody.Model
|
||||||
return &llmModel
|
return &llmModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ type Config struct {
|
|||||||
ChatAPI string `toml:"ChatAPI"`
|
ChatAPI string `toml:"ChatAPI"`
|
||||||
CompletionAPI string `toml:"CompletionAPI"`
|
CompletionAPI string `toml:"CompletionAPI"`
|
||||||
CurrentAPI string
|
CurrentAPI string
|
||||||
CurrentProvider string
|
CurrentModel string `toml:"CurrentModel"`
|
||||||
APIMap map[string]string
|
APIMap map[string]string
|
||||||
FetchModelNameAPI string `toml:"FetchModelNameAPI"`
|
FetchModelNameAPI string `toml:"FetchModelNameAPI"`
|
||||||
// ToolsAPI list?
|
// ToolsAPI list?
|
||||||
|
|||||||
@@ -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
|
// Ensure chatBody.Model is in the new list; if not, set to first available model
|
||||||
if len(newModelList) > 0 && !slices.Contains(newModelList, chatBody.Model) {
|
if len(newModelList) > 0 && !slices.Contains(newModelList, chatBody.Model) {
|
||||||
chatBody.Model = newModelList[0]
|
chatBody.Model = newModelList[0]
|
||||||
|
cfg.CurrentModel = chatBody.Model
|
||||||
// Update the displayed cell text - need to find model row
|
// Update the displayed cell text - need to find model row
|
||||||
// Search for model row by label
|
// Search for model row by label
|
||||||
for r := 0; r < table.GetRowCount(); r++ {
|
for r := 0; r < table.GetRowCount(); r++ {
|
||||||
@@ -179,6 +180,7 @@ func makePropsTable(props map[string]float32) *tview.Table {
|
|||||||
modelList := getModelListForAPI(cfg.CurrentAPI)
|
modelList := getModelListForAPI(cfg.CurrentAPI)
|
||||||
addListPopupRow("Select a model", modelList, chatBody.Model, func(option string) {
|
addListPopupRow("Select a model", modelList, chatBody.Model, func(option string) {
|
||||||
chatBody.Model = option
|
chatBody.Model = option
|
||||||
|
cfg.CurrentModel = chatBody.Model
|
||||||
})
|
})
|
||||||
// Role selection dropdown
|
// Role selection dropdown
|
||||||
addListPopupRow("Write next message as", listRolesWithUser(), cfg.WriteNextMsgAs, func(option string) {
|
addListPopupRow("Write next message as", listRolesWithUser(), cfg.WriteNextMsgAs, func(option string) {
|
||||||
|
|||||||
2
tui.go
2
tui.go
@@ -984,12 +984,14 @@ func init() {
|
|||||||
if len(ORFreeModels) > 0 {
|
if len(ORFreeModels) > 0 {
|
||||||
currentORModelIndex = (currentORModelIndex + 1) % len(ORFreeModels)
|
currentORModelIndex = (currentORModelIndex + 1) % len(ORFreeModels)
|
||||||
chatBody.Model = ORFreeModels[currentORModelIndex]
|
chatBody.Model = ORFreeModels[currentORModelIndex]
|
||||||
|
cfg.CurrentModel = chatBody.Model
|
||||||
}
|
}
|
||||||
updateStatusLine()
|
updateStatusLine()
|
||||||
} else {
|
} else {
|
||||||
if len(LocalModels) > 0 {
|
if len(LocalModels) > 0 {
|
||||||
currentLocalModelIndex = (currentLocalModelIndex + 1) % len(LocalModels)
|
currentLocalModelIndex = (currentLocalModelIndex + 1) % len(LocalModels)
|
||||||
chatBody.Model = LocalModels[currentLocalModelIndex]
|
chatBody.Model = LocalModels[currentLocalModelIndex]
|
||||||
|
cfg.CurrentModel = chatBody.Model
|
||||||
}
|
}
|
||||||
updateStatusLine()
|
updateStatusLine()
|
||||||
// // For non-OpenRouter APIs, use the old logic
|
// // For non-OpenRouter APIs, use the old logic
|
||||||
|
|||||||
Reference in New Issue
Block a user