summaryrefslogtreecommitdiff
path: root/agent
diff options
context:
space:
mode:
Diffstat (limited to 'agent')
-rw-r--r--agent/agent.go60
-rw-r--r--agent/format.go120
2 files changed, 180 insertions, 0 deletions
diff --git a/agent/agent.go b/agent/agent.go
new file mode 100644
index 0000000..30e30e3
--- /dev/null
+++ b/agent/agent.go
@@ -0,0 +1,60 @@
+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
+}
+
+// registry holds mapping from tool names to agents.
+var registry = make(map[string]Agent)
+
+// 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
+}
+
+// Get returns the agent for a tool name, or nil if none is registered.
+func Get(toolName string) Agent {
+ 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
+ }
+} \ No newline at end of file
diff --git a/agent/format.go b/agent/format.go
new file mode 100644
index 0000000..01ecb07
--- /dev/null
+++ b/agent/format.go
@@ -0,0 +1,120 @@
+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