mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-24 17:36:47 +08:00
fix(agent): return tavily tool errors as result instead of crashing the ReAct agent (#17274)
This commit is contained in:
@@ -282,11 +282,17 @@ var tavilyExtractEndpoint = "https://api.tavily.com/extract"
|
||||
|
||||
// InvokableRun performs the Tavily search. The api_key may come from the
|
||||
// argument or the TAVILY_API_KEY env var.
|
||||
//
|
||||
// All recoverable errors (API failures, network errors, missing API key) are
|
||||
// returned as a JSON envelope with an _ERROR field and a nil Go error. This
|
||||
// lets the ReAct agent's eino framework feed the error back to the LLM instead
|
||||
// of discarding the result and crashing the entire agent run. (eino's
|
||||
// wrapToolCall discards the result string when error is non-nil; returning nil
|
||||
// ensures the error context reaches the model.)
|
||||
func (t *TavilyTool) InvokableRun(ctx context.Context, argsJSON string, _ ...tool.Option) (string, error) {
|
||||
var p tavilyParams
|
||||
if err := json.Unmarshal([]byte(argsJSON), &p); err != nil {
|
||||
return tavilyErrJSON(fmt.Errorf("tavily: parse arguments: %w", err)),
|
||||
fmt.Errorf("tavily: parse arguments: %w", err)
|
||||
return tavilyErrJSON(fmt.Errorf("tavily: parse arguments: %w", err)), nil
|
||||
}
|
||||
if p.Query == "" {
|
||||
return tavilyJSON(tavilyEnvelope{Results: []map[string]any{}}), nil
|
||||
@@ -300,10 +306,13 @@ func (t *TavilyTool) InvokableRun(ctx context.Context, argsJSON string, _ ...too
|
||||
apiKey = t.envKey()
|
||||
}
|
||||
if apiKey == "" {
|
||||
return tavilyErrJSON(fmt.Errorf("tavily: api_key is required (or set TAVILY_API_KEY)")),
|
||||
fmt.Errorf("tavily: api_key is required (or set TAVILY_API_KEY)")
|
||||
return tavilyErrJSON(fmt.Errorf("tavily: api_key is required (or set TAVILY_API_KEY)")), nil
|
||||
}
|
||||
|
||||
// Match the Python implementation: always disable images and raw content.
|
||||
p.IncludeImages = false
|
||||
p.IncludeRawContent = false
|
||||
|
||||
body, _ := json.Marshal(tavilyRequestBody{
|
||||
Query: p.Query,
|
||||
MaxResults: p.MaxResults,
|
||||
@@ -323,19 +332,17 @@ func (t *TavilyTool) InvokableRun(ctx context.Context, argsJSON string, _ ...too
|
||||
map[string]string{"Authorization": "Bearer " + apiKey},
|
||||
)
|
||||
if err != nil {
|
||||
return tavilyErrJSON(err), err
|
||||
return tavilyErrJSON(err), nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return tavilyErrJSON(fmt.Errorf("tavily: upstream returned %d", resp.StatusCode)),
|
||||
fmt.Errorf("tavily: upstream returned %d", resp.StatusCode)
|
||||
return tavilyErrJSON(fmt.Errorf("tavily: upstream returned %d", resp.StatusCode)), nil
|
||||
}
|
||||
|
||||
var raw tavilyResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
|
||||
return tavilyErrJSON(fmt.Errorf("tavily: decode response: %w", err)),
|
||||
fmt.Errorf("tavily: decode response: %w", err)
|
||||
return tavilyErrJSON(fmt.Errorf("tavily: decode response: %w", err)), nil
|
||||
}
|
||||
return tavilyJSON(tavilyEnvelope{Results: raw.Results}), nil
|
||||
}
|
||||
@@ -508,17 +515,19 @@ func truncateTavilyRunes(value string, limit int) string {
|
||||
|
||||
// InvokableRun performs the Tavily Extract request. The api_key may come from
|
||||
// the argument or the TAVILY_API_KEY env var.
|
||||
//
|
||||
// All recoverable errors are returned as a JSON envelope with an _ERROR field
|
||||
// and a nil Go error, so the ReAct agent can feed the error back to the LLM
|
||||
// instead of discarding the result and crashing the agent run.
|
||||
func (t *TavilyExtractTool) InvokableRun(ctx context.Context, argsJSON string, _ ...tool.Option) (string, error) {
|
||||
var runtimeParams tavilyExtractParams
|
||||
if err := json.Unmarshal([]byte(argsJSON), &runtimeParams); err != nil {
|
||||
return tavilyExtractErrJSON(fmt.Errorf("tavily_extract: parse arguments: %w", err)),
|
||||
fmt.Errorf("tavily_extract: parse arguments: %w", err)
|
||||
return tavilyExtractErrJSON(fmt.Errorf("tavily_extract: parse arguments: %w", err)), nil
|
||||
}
|
||||
p := mergeTavilyExtractParams(t.defaults, runtimeParams)
|
||||
urls := normalizeTavilyURLs(p.URLs)
|
||||
if len(urls) == 0 {
|
||||
return tavilyExtractErrJSON(fmt.Errorf("urls is required")),
|
||||
fmt.Errorf("tavily_extract: urls is required")
|
||||
return tavilyExtractErrJSON(fmt.Errorf("urls is required")), nil
|
||||
}
|
||||
if p.ExtractDepth == "" {
|
||||
p.ExtractDepth = "basic"
|
||||
@@ -532,8 +541,7 @@ func (t *TavilyExtractTool) InvokableRun(ctx context.Context, argsJSON string, _
|
||||
apiKey = t.envKey()
|
||||
}
|
||||
if apiKey == "" {
|
||||
return tavilyExtractErrJSON(fmt.Errorf("tavily_extract: api_key is required (or set TAVILY_API_KEY)")),
|
||||
fmt.Errorf("tavily_extract: api_key is required (or set TAVILY_API_KEY)")
|
||||
return tavilyExtractErrJSON(fmt.Errorf("tavily_extract: api_key is required (or set TAVILY_API_KEY)")), nil
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(tavilyExtractRequestBody{
|
||||
@@ -547,19 +555,17 @@ func (t *TavilyExtractTool) InvokableRun(ctx context.Context, argsJSON string, _
|
||||
map[string]string{"Authorization": "Bearer " + apiKey},
|
||||
)
|
||||
if err != nil {
|
||||
return tavilyExtractErrJSON(err), err
|
||||
return tavilyExtractErrJSON(err), nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return tavilyExtractErrJSON(fmt.Errorf("tavily_extract: upstream returned %d", resp.StatusCode)),
|
||||
fmt.Errorf("tavily_extract: upstream returned %d", resp.StatusCode)
|
||||
return tavilyExtractErrJSON(fmt.Errorf("tavily_extract: upstream returned %d", resp.StatusCode)), nil
|
||||
}
|
||||
|
||||
var raw tavilyExtractResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
|
||||
return tavilyExtractErrJSON(fmt.Errorf("tavily_extract: decode response: %w", err)),
|
||||
fmt.Errorf("tavily_extract: decode response: %w", err)
|
||||
return tavilyExtractErrJSON(fmt.Errorf("tavily_extract: decode response: %w", err)), nil
|
||||
}
|
||||
return tavilyExtractJSON(tavilyExtractEnvelope{Results: raw.Results}), nil
|
||||
}
|
||||
|
||||
@@ -79,8 +79,11 @@ func TestTavily_BuildRequest(t *testing.T) {
|
||||
if gotBody["search_depth"] != "advanced" {
|
||||
t.Errorf("body.search_depth = %v, want advanced", gotBody["search_depth"])
|
||||
}
|
||||
if gotBody["include_raw_content"] != true || gotBody["include_images"] != true {
|
||||
t.Errorf("include flags = raw:%v images:%v, want true/true", gotBody["include_raw_content"], gotBody["include_images"])
|
||||
// include_raw_content and include_images are always forced to false
|
||||
// to match the Python implementation (avoids bloating the context
|
||||
// with base64 image data).
|
||||
if gotBody["include_raw_content"] != false || gotBody["include_images"] != false {
|
||||
t.Errorf("include flags = raw:%v images:%v, want false/false (forced)", gotBody["include_raw_content"], gotBody["include_images"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,12 +136,12 @@ func TestTavily_RequiresAPIKey(t *testing.T) {
|
||||
// envKey always returns "" so we know the failure is from the
|
||||
// missing api_key, not from a stray process env var.
|
||||
tool := NewTavilyToolWithEnvKey(NewHTTPHelper(), func() string { return "" })
|
||||
_, err := tool.InvokableRun(context.Background(), `{"query":"x"}`)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing api_key")
|
||||
out, err := tool.InvokableRun(context.Background(), `{"query":"x"}`)
|
||||
if err != nil {
|
||||
t.Fatalf("InvokableRun should not return a Go error for missing api_key: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "api_key") {
|
||||
t.Errorf("err = %v, want to mention api_key", err)
|
||||
if !strings.Contains(out, "api_key") {
|
||||
t.Errorf("output = %q, want to mention api_key", out)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,19 +266,22 @@ func TestTavily_ExplicitFlagsOverrideNodeDefaults(t *testing.T) {
|
||||
|
||||
helper := NewHTTPHelper().WithClient(&http.Client{Transport: rewriteHostTransport(srv.URL)})
|
||||
tavily := newTavilyTool(helper, func() string { return "" }, tavilyParams{
|
||||
APIKey: "stored-key", IncludeRawContent: true, IncludeImages: true,
|
||||
APIKey: "stored-key", IncludeAnswer: true,
|
||||
})
|
||||
if _, err := tavily.InvokableRun(context.Background(), `{"query":"ragflow"}`); err != nil {
|
||||
t.Fatalf("InvokableRun(node defaults): %v", err)
|
||||
}
|
||||
if gotBody["include_raw_content"] != true || gotBody["include_images"] != true {
|
||||
t.Fatalf("node default flags were not sent: %#v", gotBody)
|
||||
}
|
||||
if _, err := tavily.InvokableRun(context.Background(), `{"query":"ragflow","include_raw_content":false,"include_images":false}`); err != nil {
|
||||
t.Fatalf("InvokableRun: %v", err)
|
||||
if gotBody["include_answer"] != true {
|
||||
t.Fatalf("node default include_answer was not sent: %#v", gotBody)
|
||||
}
|
||||
if gotBody["include_raw_content"] != false || gotBody["include_images"] != false {
|
||||
t.Fatalf("explicit false flags were not preserved: %#v", gotBody)
|
||||
t.Fatalf("include_raw_content/images should be forced false: %#v", gotBody)
|
||||
}
|
||||
if _, err := tavily.InvokableRun(context.Background(), `{"query":"ragflow","include_answer":false}`); err != nil {
|
||||
t.Fatalf("InvokableRun: %v", err)
|
||||
}
|
||||
if gotBody["include_answer"] != false {
|
||||
t.Fatalf("explicit false include_answer was not preserved: %#v", gotBody)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,12 +432,12 @@ func TestTavilyExtract_RequiresAPIKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tool := NewTavilyExtractToolWithEnvKey(NewHTTPHelper(), func() string { return "" })
|
||||
_, err := tool.InvokableRun(context.Background(), `{"urls":["https://a.example/"]}`)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing api_key")
|
||||
out, err := tool.InvokableRun(context.Background(), `{"urls":["https://a.example/"]}`)
|
||||
if err != nil {
|
||||
t.Fatalf("InvokableRun should not return a Go error for missing api_key: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "api_key") {
|
||||
t.Errorf("err = %v, want to mention api_key", err)
|
||||
if !strings.Contains(out, "api_key") {
|
||||
t.Errorf("output = %q, want to mention api_key", out)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user