mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-09 04:44:49 +08:00
Moves doc_id blob resolution into Parser, tightens chunker/tokenizer to Python output_format semantics, updates extractor list handling, and fixes real-template integration tests.
207 lines
8.2 KiB
Go
207 lines
8.2 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.
|
|
//
|
|
|
|
// runtime — category-aware component registry.
|
|
//
|
|
// Phase 0 of plan port-rag-flow-pipeline-to-go.md lifts the component
|
|
// registry out of internal/agent/component into the runtime package
|
|
// so the ingestion pipeline (Phase 2) can register under
|
|
// CategoryIngestion without depending on the agent canvas. The legacy
|
|
// component.Register / component.New / component.RegisteredNames become
|
|
// thin adapters that delegate here.
|
|
//
|
|
// The single source of truth is DefaultRegistry. The component package's
|
|
// internal `registry` map has been removed — keeping two maps in sync
|
|
// during the transition would cause RegisteredNames() to return a
|
|
// partial set (the internal map only sees legacy Register calls; the
|
|
// new map only sees RegisterWithMeta calls). See plan §5.
|
|
package runtime
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Category tags each registered component with its domain so the UI can
|
|
// filter and the runtime can audit cross-domain wiring.
|
|
type Category string
|
|
|
|
const (
|
|
CategoryAgent Category = "agent"
|
|
CategoryIngestion Category = "ingestion"
|
|
CategoryShared Category = "shared"
|
|
)
|
|
|
|
// Metadata is the static component descriptor exposed to the API.
|
|
// Components MUST provide this at registration time so the API can serve
|
|
// a complete component catalog (name, category, inputs, outputs) without
|
|
// having to instantiate the component.
|
|
//
|
|
// The plan §4 Phase 0 task 1 contract says Register REJECTS empty
|
|
// metadata. The "is empty" check is implemented in Register as
|
|
// "Version == \"\" AND Inputs == nil AND Outputs == nil"; this
|
|
// three-way check lets a caller fill in any single field as
|
|
// evidence of intent. The Version field is the canonical marker
|
|
// (plan §4 Phase 0 task 2 uses `Metadata{Version: "legacy"}` for
|
|
// the legacy-adapter shim), but a component that wants to skip
|
|
// the version stamp but still record inputs/outputs is also
|
|
// allowed.
|
|
type Metadata struct {
|
|
Version string // contract version; required for ingestion, "legacy" for the legacy adapter
|
|
Inputs map[string]string // input key → human-readable description
|
|
Outputs map[string]string // output key → human-readable description
|
|
}
|
|
|
|
// entry is one slot in the registry.
|
|
type entry struct {
|
|
factory ComponentFactory
|
|
category Category
|
|
metadata Metadata
|
|
}
|
|
|
|
// Registry is the process-wide collection of named ComponentFactories,
|
|
// tagged with Category so callers can enumerate by domain. Each registration
|
|
// also carries static Metadata (Inputs/Outputs) consumed by the API layer.
|
|
type Registry interface {
|
|
Register(name string, category Category, factory ComponentFactory, metadata Metadata) error
|
|
Lookup(name string) (ComponentFactory, Category, Metadata, bool)
|
|
NamesByCategory(category Category) []string
|
|
Names() []string
|
|
}
|
|
|
|
// memoryRegistry is the production Registry. It is concurrency-safe; init()
|
|
// race-to-register is acceptable for `Register` — the duplicate-registration
|
|
// check rejects the second writer, so the FIRST successful registration
|
|
// for a given name wins. (This is independent of the `SetDefaultFactory`
|
|
// "first-wins" guard, which is REMOVED in Phase 0 task 3; see
|
|
// "two-layer model" comment there for the distinction.)
|
|
//
|
|
// Lookup is **case-insensitive**: keys are lowercased on Register and Lookup.
|
|
// This matches internal/agent/component/registry.go:28, 43, which lowercases
|
|
// at both ends. A case-sensitive implementation would silently fail canvas
|
|
// build with "unknown component" errors when an existing init() registers
|
|
// "ExampleComponent" but the canvas looks up "examplecomponent" (or vice
|
|
// versa).
|
|
type memoryRegistry struct {
|
|
mu sync.RWMutex
|
|
entries map[string]entry
|
|
}
|
|
|
|
// Register enrolls a ComponentFactory under name (case-insensitive).
|
|
// Returns an error on empty name, empty metadata, or duplicate key —
|
|
// callers who want the legacy panic-on-duplicate semantics at init()
|
|
// time should wrap the call with MustRegister.
|
|
//
|
|
// "Empty metadata" (plan §4 Phase 0 task 1) means all three of
|
|
// Version, Inputs, Outputs are unset. The Version field is the
|
|
// canonical marker — ingestion components MUST supply a real
|
|
// version string; the legacy agent-component adapter MUST supply
|
|
// "legacy"; the catalog endpoint only serves components whose
|
|
// metadata passes this check.
|
|
func (r *memoryRegistry) Register(name string, category Category, factory ComponentFactory, metadata Metadata) error {
|
|
key := strings.ToLower(strings.TrimSpace(name))
|
|
if key == "" {
|
|
return fmt.Errorf("runtime: Register called with empty name")
|
|
}
|
|
if metadata.Version == "" && metadata.Inputs == nil && metadata.Outputs == nil {
|
|
return fmt.Errorf("runtime: %q registered with empty metadata (Version, Inputs, Outputs all unset; "+
|
|
"see plan §4 Phase 0 task 1 — ingestion components MUST supply a version string, "+
|
|
"legacy agent-component adapter MUST supply {Version: \"legacy\"})", name)
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if _, exists := r.entries[key]; exists {
|
|
return fmt.Errorf("runtime: %q already registered", name)
|
|
}
|
|
r.entries[key] = entry{factory: factory, category: category, metadata: metadata}
|
|
return nil
|
|
}
|
|
|
|
// Lookup resolves a name (case-insensitive) to its factory + category +
|
|
// metadata. Returns ok=false on miss.
|
|
func (r *memoryRegistry) Lookup(name string) (ComponentFactory, Category, Metadata, bool) {
|
|
key := strings.ToLower(strings.TrimSpace(name))
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
e, ok := r.entries[key]
|
|
if !ok {
|
|
return nil, "", Metadata{}, false
|
|
}
|
|
return e.factory, e.category, e.metadata, true
|
|
}
|
|
|
|
// NamesByCategory returns the sorted list of names registered under the
|
|
// given category. Sorted output keeps UI listings and error messages
|
|
// stable.
|
|
func (r *memoryRegistry) NamesByCategory(category Category) []string {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
out := make([]string, 0, len(r.entries))
|
|
for n, e := range r.entries {
|
|
if e.category == category {
|
|
out = append(out, n)
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// Names returns the sorted list of all registered names.
|
|
func (r *memoryRegistry) Names() []string {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
out := make([]string, 0, len(r.entries))
|
|
for n := range r.entries {
|
|
out = append(out, n)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// NewMemoryRegistry constructs an empty in-memory Registry. Tests use
|
|
// this to spin up isolated registries; production code uses
|
|
// DefaultRegistry.
|
|
func NewMemoryRegistry() Registry {
|
|
return &memoryRegistry{entries: make(map[string]entry)}
|
|
}
|
|
|
|
// DefaultRegistry is the process-wide singleton. Each component package's
|
|
// init() registers its factories here. Lookup is lazy: even if a canvas
|
|
// build occurs before every package's init() has run, lookups see all
|
|
// completed registrations because the registry is read at call time, not
|
|
// at SetDefaultFactory time.
|
|
var DefaultRegistry Registry = NewMemoryRegistry()
|
|
|
|
// MustRegister wraps Register and panics on error. Init()-time callers
|
|
// that want the legacy "panic on duplicate" behaviour can use this
|
|
// instead of Register + manual error check.
|
|
func MustRegister(name string, category Category, factory ComponentFactory, metadata Metadata) {
|
|
if err := DefaultRegistry.Register(name, category, factory, metadata); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// RegisterWithMeta is a thin convenience around DefaultRegistry.Register
|
|
// for callers that want explicit (name, category, factory, metadata) at
|
|
// the call site without going through MustRegister. It returns the same
|
|
// error as Register; callers that want panic-on-error should use
|
|
// MustRegister instead.
|
|
func RegisterWithMeta(name string, category Category, factory ComponentFactory, metadata Metadata) error {
|
|
return DefaultRegistry.Register(name, category, factory, metadata)
|
|
}
|