web_fetch.go 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. package tools
  2. import (
  3. "context"
  4. _ "embed"
  5. "fmt"
  6. "net/http"
  7. "os"
  8. "strings"
  9. "time"
  10. "charm.land/fantasy"
  11. )
  12. //go:embed web_fetch.md
  13. var webFetchToolDescription []byte
  14. // NewWebFetchTool creates a simple web fetch tool for sub-agents (no permissions needed).
  15. func NewWebFetchTool(workingDir string, client *http.Client) fantasy.AgentTool {
  16. if client == nil {
  17. transport := http.DefaultTransport.(*http.Transport).Clone()
  18. transport.MaxIdleConns = 100
  19. transport.MaxIdleConnsPerHost = 10
  20. transport.IdleConnTimeout = 90 * time.Second
  21. client = &http.Client{
  22. Timeout: 30 * time.Second,
  23. Transport: transport,
  24. }
  25. }
  26. return fantasy.NewParallelAgentTool(
  27. WebFetchToolName,
  28. FirstLineDescription(webFetchToolDescription),
  29. func(ctx context.Context, params WebFetchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
  30. if params.URL == "" {
  31. return fantasy.NewTextErrorResponse("url is required"), nil
  32. }
  33. content, err := FetchURLAndConvert(ctx, client, params.URL)
  34. if err != nil {
  35. return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to fetch URL: %s", err)), nil
  36. }
  37. hasLargeContent := len(content) > LargeContentThreshold
  38. var result strings.Builder
  39. if hasLargeContent {
  40. tempFile, err := os.CreateTemp(workingDir, "page-*.md")
  41. if err != nil {
  42. return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to create temporary file: %s", err)), nil
  43. }
  44. tempFilePath := tempFile.Name()
  45. if _, err := tempFile.WriteString(content); err != nil {
  46. _ = tempFile.Close() // Best effort close
  47. return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to write content to file: %s", err)), nil
  48. }
  49. if err := tempFile.Close(); err != nil {
  50. return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to close temporary file: %s", err)), nil
  51. }
  52. fmt.Fprintf(&result, "Fetched content from %s (large page)\n\n", params.URL)
  53. fmt.Fprintf(&result, "Content saved to: %s\n\n", tempFilePath)
  54. result.WriteString("Use the view and grep tools to analyze this file.")
  55. } else {
  56. fmt.Fprintf(&result, "Fetched content from %s:\n\n", params.URL)
  57. result.WriteString(content)
  58. }
  59. return fantasy.NewTextResponse(result.String()), nil
  60. })
  61. }