batch.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. package tools
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "strings"
  7. "sync"
  8. )
  9. type BatchToolCall struct {
  10. Name string `json:"name"`
  11. Input json.RawMessage `json:"input"`
  12. }
  13. type BatchParams struct {
  14. Calls []BatchToolCall `json:"calls"`
  15. }
  16. type BatchToolResult struct {
  17. ToolName string `json:"tool_name"`
  18. ToolInput json.RawMessage `json:"tool_input"`
  19. Result json.RawMessage `json:"result"`
  20. Error string `json:"error,omitempty"`
  21. // Added for better formatting and separation between results
  22. Separator string `json:"separator,omitempty"`
  23. }
  24. type BatchResult struct {
  25. Results []BatchToolResult `json:"results"`
  26. }
  27. type batchTool struct {
  28. tools map[string]BaseTool
  29. }
  30. const (
  31. BatchToolName = "batch"
  32. BatchToolDescription = `Executes multiple tool calls in parallel and returns their results.
  33. WHEN TO USE THIS TOOL:
  34. - Use when you need to run multiple independent tool calls at once
  35. - Helpful for improving performance by parallelizing operations
  36. - Great for gathering information from multiple sources simultaneously
  37. HOW TO USE:
  38. - Provide an array of tool calls, each with a name and input
  39. - Each tool call will be executed in parallel
  40. - Results are returned in the same order as the input calls
  41. FEATURES:
  42. - Runs tool calls concurrently for better performance
  43. - Returns both results and errors for each call
  44. - Maintains the order of results to match input calls
  45. LIMITATIONS:
  46. - All tools must be available in the current context
  47. - Complex error handling may be required for some use cases
  48. - Not suitable for tool calls that depend on each other's results
  49. TIPS:
  50. - Use for independent operations like multiple file reads or searches
  51. - Great for batch operations like searching multiple directories
  52. - Combine with other tools for more complex workflows`
  53. )
  54. func NewBatchTool(tools map[string]BaseTool) BaseTool {
  55. return &batchTool{
  56. tools: tools,
  57. }
  58. }
  59. func (b *batchTool) Info() ToolInfo {
  60. return ToolInfo{
  61. Name: BatchToolName,
  62. Description: BatchToolDescription,
  63. Parameters: map[string]any{
  64. "calls": map[string]any{
  65. "type": "array",
  66. "description": "Array of tool calls to execute in parallel",
  67. "items": map[string]any{
  68. "type": "object",
  69. "properties": map[string]any{
  70. "name": map[string]any{
  71. "type": "string",
  72. "description": "Name of the tool to call",
  73. },
  74. "input": map[string]any{
  75. "type": "object",
  76. "description": "Input parameters for the tool",
  77. },
  78. },
  79. "required": []string{"name", "input"},
  80. },
  81. },
  82. },
  83. Required: []string{"calls"},
  84. }
  85. }
  86. func (b *batchTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
  87. var params BatchParams
  88. if err := json.Unmarshal([]byte(call.Input), &params); err != nil {
  89. return NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
  90. }
  91. if len(params.Calls) == 0 {
  92. return NewTextErrorResponse("no tool calls provided"), nil
  93. }
  94. var wg sync.WaitGroup
  95. results := make([]BatchToolResult, len(params.Calls))
  96. for i, toolCall := range params.Calls {
  97. wg.Add(1)
  98. go func(index int, tc BatchToolCall) {
  99. defer wg.Done()
  100. // Create separator for better visual distinction between results
  101. separator := ""
  102. if index > 0 {
  103. separator = fmt.Sprintf("\n%s\n", strings.Repeat("=", 80))
  104. }
  105. result := BatchToolResult{
  106. ToolName: tc.Name,
  107. ToolInput: tc.Input,
  108. Separator: separator,
  109. }
  110. tool, ok := b.tools[tc.Name]
  111. if !ok {
  112. result.Error = fmt.Sprintf("tool not found: %s", tc.Name)
  113. results[index] = result
  114. return
  115. }
  116. // Create a proper ToolCall object
  117. callObj := ToolCall{
  118. ID: fmt.Sprintf("batch-%d", index),
  119. Name: tc.Name,
  120. Input: string(tc.Input),
  121. }
  122. response, err := tool.Run(ctx, callObj)
  123. if err != nil {
  124. result.Error = fmt.Sprintf("error executing tool %s: %s", tc.Name, err)
  125. results[index] = result
  126. return
  127. }
  128. // Standardize metadata format if present
  129. if response.Metadata != "" {
  130. var metadata map[string]interface{}
  131. if err := json.Unmarshal([]byte(response.Metadata), &metadata); err == nil {
  132. // Add tool name to metadata for better context
  133. metadata["tool"] = tc.Name
  134. // Re-marshal with consistent formatting
  135. if metadataBytes, err := json.MarshalIndent(metadata, "", " "); err == nil {
  136. response.Metadata = string(metadataBytes)
  137. }
  138. }
  139. }
  140. // Convert the response to JSON
  141. responseJSON, err := json.Marshal(response)
  142. if err != nil {
  143. result.Error = fmt.Sprintf("error marshaling response: %s", err)
  144. results[index] = result
  145. return
  146. }
  147. result.Result = responseJSON
  148. results[index] = result
  149. }(i, toolCall)
  150. }
  151. wg.Wait()
  152. batchResult := BatchResult{
  153. Results: results,
  154. }
  155. resultJSON, err := json.Marshal(batchResult)
  156. if err != nil {
  157. return NewTextErrorResponse(fmt.Sprintf("error marshaling batch result: %s", err)), nil
  158. }
  159. return NewTextResponse(string(resultJSON)), nil
  160. }