mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-05 07:10:29 +08:00
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.
108 lines
3.5 KiB
Go
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
|
|
}
|