Files
ragflow/internal/agent/harness/agentic_rag.go
Zhichang Yu 4e78f1f440 Port Python agentic search to Go (nav service, harness, tools) (#17702)
Port Python rag/advanced_rag agentic search to Go: ES-backed dataset-nav
service, agentic-search harness, and agent tools.

Includes agentic-search port plan and self-review docs.
2026-08-03 11:16:16 +08:00

108 lines
3.5 KiB
Go

//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package harness
import (
"context"
"log"
"strings"
"gorm.io/gorm"
)
// AgenticState carries the shared state across the agentic-RAG graph nodes.
type AgenticState struct {
Question string
Keywords string
Route RouteDecision
SeedChunks []string
Plan WorkflowPlan
Kbinfos *Kbinfos
PartialAnswer bool
Abstain bool
EmptyResult bool
FinalAnswer string
FormalizeError string
}
// RunAgenticRAG drives the agentic-search graph: route → pre_search → planner →
// orchestrator → formalize_answer. Mirrors build_agentic_graph's linear flow.
//
// - low (direct_search): one hybrid search → answer.
// - medium+ (decompose_and_search / agentic_research / deep_research):
// pre_search grounds the planner, then decompose-and-search runs until a
// sufficiency verdict stops it.
func RunAgenticRAG(ctx context.Context, db *gorm.DB, question, keywords, modeLabel string, search SearchFn) AnswerResult {
state := &AgenticState{
Question: strings.TrimSpace(question),
Keywords: keywords,
Kbinfos: &Kbinfos{},
}
if state.Question == "" {
return AnswerResult{FinalAnswer: emptyResultMessage, Empty: true}
}
// ── route ──
state.Route = RouteNode(ctx, db, state.Question, modeLabel)
// ── pre_search (decomposition modes only) ──
if state.Route.RequiresDecomposition {
chunks, aggs := search(ctx, state.Question, state.Keywords)
state.SeedChunks = extractChunkTexts(chunks)
state.Kbinfos.Merge(chunks, aggs)
}
// ── planner ──
state.Plan = PlannerNode(ctx, db, state.Route, state.SeedChunks)
// ── orchestrator ──
var orch OrchestratorResult
if state.Route.RequiresDecomposition {
claims := make([]*ClaimTarget, len(state.Plan.Claims))
for i := range state.Plan.Claims {
claims[i] = &state.Plan.Claims[i]
}
orch = DecomposeAndSearch(ctx, search, state.Question, state.Keywords, claims, modeLabel, state.Kbinfos)
} else {
orch = DirectSearch(ctx, search, state.Question, state.Keywords, state.Kbinfos)
}
state.PartialAnswer = orch.PartialAnswer
state.Abstain = orch.Abstain
state.EmptyResult = orch.EmptyResult
if orch.Kbinfos != nil {
state.Kbinfos = orch.Kbinfos
}
// ── formalize_answer ──
res := FormalizeAnswer(ctx, db, state.Question, state.Kbinfos, state.PartialAnswer, state.Abstain, state.EmptyResult)
// Log only the question length, never its content, to avoid persisting user
// input in logs.
log.Printf("agentic_rag: finished (qlen=%d, strategy=%s, chunks=%d, partial=%v, abstain=%v)",
len(state.Question), state.Route.ExecutionStrategy, len(state.Kbinfos.Chunks), state.PartialAnswer, state.Abstain)
return res
}
func extractChunkTexts(chunks []map[string]interface{}) []string {
out := make([]string, 0, len(chunks))
for _, c := range chunks {
if t := chunkText(c); t != "" {
out = append(out, t)
}
}
return out
}