feat(agent): add Querit Contents tool (#18156)

### Summary

Add Querit Contents as a built-in page content tool for RAGFlow Agents
and Canvas workflows.

Querit Contents crawls one or more known URLs and returns their page
content and optional metadata. It complements the existing Querit Search
tool: Search discovers relevant pages, while Contents retrieves pages
already selected by an Agent or workflow.

This integration supports two usage modes:

- A standalone `QueritContents` node in Canvas workflows.
- An embedded content tool available to RAGFlow Agents.
This commit is contained in:
EthanZhang
2026-08-12 21:03:21 +08:00
committed by GitHub
parent c7db20b8d3
commit 1d8b9c7452
26 changed files with 1200 additions and 88 deletions

View File

@@ -248,44 +248,58 @@ func (q *QueritTool) InvokableRun(ctx context.Context, argsJSON string, _ ...too
if err != nil {
return queritErrorJSON(fmt.Errorf("encode request: %w", err), apiKey), nil
}
raw, requestErr := doQueritRequest(ctx, q.helper, q.retryWait, queritEndpoint, body, apiKey)
if requestErr != nil {
return queritErrorJSON(requestErr, apiKey), nil
}
if _, err := decodeQueritResponse(raw); err != nil {
return queritErrorJSON(err, apiKey), nil
}
return string(raw), nil
}
func doQueritRequest(
ctx context.Context,
helper *HTTPHelper,
retryWait func(context.Context, int) bool,
endpoint string,
body []byte,
apiKey string,
) ([]byte, error) {
for attempt := 1; attempt <= queritMaxAttempts; attempt++ {
resp, requestErr := q.helper.Do(
resp, err := helper.Do(
ctx,
http.MethodPost,
queritEndpoint,
endpoint,
string(body),
"application/json",
map[string]string{"Authorization": "Bearer " + apiKey},
)
if requestErr != nil {
return queritErrorJSON(fmt.Errorf("request failed: %w", requestErr), apiKey), nil
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
if resp.StatusCode == http.StatusTooManyRequests {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
if attempt == queritMaxAttempts || !q.retryWait(ctx, attempt) {
return queritErrorJSON(fmt.Errorf("upstream returned %d after %d attempts", resp.StatusCode, attempt), apiKey), nil
if attempt == queritMaxAttempts || !retryWait(ctx, attempt) {
return nil, fmt.Errorf("upstream returned %d after %d attempts", resp.StatusCode, attempt)
}
continue
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
return queritErrorJSON(fmt.Errorf("upstream returned %d", resp.StatusCode), apiKey), nil
return nil, fmt.Errorf("upstream returned %d", resp.StatusCode)
}
raw, readErr := io.ReadAll(resp.Body)
_ = resp.Body.Close()
if readErr != nil {
return queritErrorJSON(fmt.Errorf("read response: %w", readErr), apiKey), nil
return nil, fmt.Errorf("read response: %w", readErr)
}
if _, err := decodeQueritResponse(raw); err != nil {
return queritErrorJSON(err, apiKey), nil
}
return string(raw), nil
return raw, nil
}
return queritErrorJSON(fmt.Errorf("request exhausted retries"), apiKey), nil
return nil, fmt.Errorf("request exhausted retries")
}
func mergeQueritParams(defaults, runtimeParams queritParams, provided map[string]json.RawMessage) queritParams {
@@ -360,21 +374,9 @@ func buildQueritRequest(params queritParams) queritRequest {
}
func decodeQueritResponse(raw []byte) (map[string]any, error) {
var decoded any
decoder := json.NewDecoder(strings.NewReader(string(raw)))
decoder.UseNumber()
if err := decoder.Decode(&decoded); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
if err == nil {
return nil, fmt.Errorf("decode response: expected exactly one JSON value")
}
return nil, fmt.Errorf("decode response: trailing content: %w", err)
}
response, ok := decoded.(map[string]any)
if !ok || response == nil {
return nil, fmt.Errorf("decode response: expected a JSON object")
response, err := decodeQueritJSONObject(raw)
if err != nil {
return nil, err
}
resultsValue, exists := response["results"]
if !exists {
@@ -394,6 +396,26 @@ func decodeQueritResponse(raw []byte) (map[string]any, error) {
return response, nil
}
func decodeQueritJSONObject(raw []byte) (map[string]any, error) {
var decoded any
decoder := json.NewDecoder(strings.NewReader(string(raw)))
decoder.UseNumber()
if err := decoder.Decode(&decoded); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
if err == nil {
return nil, fmt.Errorf("decode response: expected exactly one JSON value")
}
return nil, fmt.Errorf("decode response: trailing content: %w", err)
}
response, ok := decoded.(map[string]any)
if !ok || response == nil {
return nil, fmt.Errorf("decode response: expected a JSON object")
}
return response, nil
}
func waitForQueritRetry(ctx context.Context, attempt int) bool {
delay := 200 * time.Millisecond
for current := 1; current < attempt; current++ {
@@ -588,9 +610,13 @@ func queritStringSlice(value any) ([]string, bool) {
}
func queritErrorJSON(err error, apiKeys ...string) string {
message := "querit_search: unknown error"
return queritToolErrorJSON(queritToolName, err, apiKeys...)
}
func queritToolErrorJSON(toolName string, err error, apiKeys ...string) string {
message := toolName + ": unknown error"
if err != nil {
message = "querit_search: " + err.Error()
message = toolName + ": " + err.Error()
}
for _, apiKey := range apiKeys {
if apiKey = strings.TrimSpace(apiKey); apiKey != "" {
@@ -599,7 +625,7 @@ func queritErrorJSON(err error, apiKeys ...string) string {
}
raw, marshalErr := json.Marshal(map[string]any{"_ERROR": message})
if marshalErr != nil {
return `{"_ERROR":"querit_search: marshal error"}`
return fmt.Sprintf(`{"_ERROR":%q}`, toolName+": marshal error")
}
return string(raw)
}