Agent Canvas/Component System/ — Component System

Modular, extensible component model for workflow steps

ComponentBase lifecycle (init, validate, run, serialize); ComponentParamBase for validation; registry for lookup; 22 component types (Begin, LLM, Agent, Switch, Iteration, Loop, Message, Invoke, and 14 more).

N
Classes
N
Files
~N k
Lines
N
Key Metric

📚 What is this & why does it exist?

Components are the fundamental building blocks of workflows. Each component is a stateful object with inputs, outputs, and a lifecycle. Without components, every workflow requires custom code—the model enables reuse and UI-driven building. — Replace with 2-4 plain-English sentences explaining what this sub-module does and why it exists. No jargon. Target audience: someone who hasn't read the code.

💡 Real-world analogy

Like OOP libraries: ComponentBase = abstract base class; LLM, Agent, Switch = concrete subclasses; each has __init__() and run(). Registry is the module namespace. — Replace with a vivid, concrete metaphor mapping this sub-module's core concept to everyday life (restaurant, airport, library, highway, etc.). 3-5 sentences.

🤔 Why Is It Designed This Way?

Components are stateful (not pure) to support memory across steps—essential for multi-turn LLM chats. Parameters validated at load-time, catching errors early. Registry uses factories (not reflection) for safety. — 150+ words explaining the design choices: what pattern does this follow and why? What constraints shaped the implementation? What trade-offs were made and what was sacrificed?

Design ChoiceWhat Was ChosenWhat Was RejectedWhy
Component modelStateful objects with memoryPure functional transformsStateful enables multi-turn LLM memory; functional is easier to parallelize but loses context
Parameter validationLoad-time schema checksRuntime type coercionLoad-time catches errors early; runtime is more flexible but hides bugs
Registry mechanismFactory functions (explicit)Class import via reflectionFactories are safe; reflection risks circular imports and side effects
Output cachingImmutable cache per componentRe-compute on every lookupCaching avoids redundant work; re-compute is simpler but wastes CPU for repeated accesses

Contents

Architecture

graph TB
    BASE["ComponentBase
Abstract Lifecycle"] PARAM["ComponentParamBase
Parameter Schema"] BASE --> PARAM REGISTRY["Component
Registry"] BASE --> REGISTRY BEGIN["Begin
Entry"] --> REGISTRY LLM["LLM
Chat"] --> REGISTRY AGENT["Agent
Tools"] --> REGISTRY SWITCH["Switch
Route"] --> REGISTRY ITER["Iteration
Loop"] --> REGISTRY LOOP["Loop
Struct"] --> REGISTRY MSG["Message
Format"] --> REGISTRY INVOKE["Invoke
Direct"] --> REGISTRY

Component Lifecycle

flowchart TD
    Init["__init__
Configure"] Validate["validate_params
Schema Check"] Run["run()
Execute"] Cache["Output Cache
Stored"] Serialize["to_dict()
Serialize"] Init --> Validate Validate --> Run Run --> Cache Cache --> Serialize style Init fill:#c8e6c9 style Cache fill:#ffccbc style Serialize fill:#b3e5fc

Key Classes

Class / FunctionFilePurpose
ComponentBasecomponent/base.pyAbstract base with init, validate_params, run, to_dict; all components inherit
ComponentParamBasecomponent/base.pyNested class defining input/output schemas with types, defaults, descriptions
LLMcomponent/llm.pyCalls chat model (OpenAI, Anthropic); supports structured output (JSON Schema)
Agentcomponent/agent_with_tools.pyExtends LLM; adds tool-calling loop; LLM suggests tool, Agent calls it, loops until stop
Switchcomponent/switch.pyEvaluates conditions; routes to multiple next-step branches; fan-out support

Data Structures

TypeNameDescription
dictparamsConfiguration dict for component (e.g., {model: "gpt-4", temperature: 0.7})
dict_cacheOutput cache; stores latest run output; reused by downstream components
dict_inputs_historyAll inputs ever passed to this component; for debugging and replay
listschema.inputsParameter schema: [{name, type, default, description}]
listschema.outputsOutput schema: [{name, type, description}]

Interactions

DirectionPartnerInterfaceDescription
→Canvascomponent.run(inputs)Canvas passes resolved inputs; component executes and returns outputs
→Tool Registrytool_registry.get(name)Agent component looks up and calls tools by name
→Param Schemaschema.validate(params)Canvas calls at load-time to validate parameter types and presence
←UI Builderschema.to_html_form()UI auto-generates form from param schema; users configure visually

Configuration

Knob / ParameterDefaultDescription
max_retries3Number of retry attempts on failure; exponential backoff applied
exception_default_valuenullIf all retries fail, return this value instead of propagating error
exception_gotonullIf error occurs, jump to this next-step node (error handling branch)
timeout_sec30Maximum execution time; raises TimeoutError if exceeded

Directory Map

Source tree for Component System
submod_name/
├── file_a.ext                 ← Role: brief annotation
├── file_b.ext                 ← Role: brief annotation
├── file_c.ext                 ← Role: brief annotation
└── index.ext                  ← Role: entry point

📑 Annotated Code Walkthrough

The most important code in this sub-module, annotated with WHY each piece exists:

run() — Execute component with resolved inputs

// Replace with actual code from the source
void key_function() {
    // ← WHY: Explain the purpose of this line
    // ← WHY: What constraint or requirement drives this?
}

Line-by-line:

  • Line N: What this does and WHY — what would break if removed
  • Line M: The design choice here — why this approach over the obvious alternative

⚠️ Edge Cases & Boundaries

Edge CaseWhat HappensWhere in CodeRisk Level
Empty input dictComponent receives no inputs; uses param defaults; may fail if required params missingComponentBase.run()🔴 High — component must handle gracefully or validate earlier
Component name collisionTwo components have same ID (cpn_id); Canvas._outputs overwrites firstCanvas.load()🔴 High — Canvas validates unique IDs; error if duplicate detected
Circular component dependencyComponent A depends on output of B, which depends on A (cycle at component level, not node level)Canvas.topological_sort()🔴 High — topological sort detects and rejects cyclic DAGs
LLM streaming timeoutLLM returns stream but drops connection mid-stream; partial output bufferedLLM.run()🔴 High — stream handler catches and propagates error; retry logic applies
Concurrent accessHow it's handledfunction_name:line🔴 High

Deep-Dive Highlights

Core mechanisms from this sub-module have been extracted into dedicated focus pages for in-depth exploration. Each page covers one algorithm, protocol, or subsystem with diagrams, code walks, and configuration details.

Deep-Dive Pages

LLM Component Deep-Dive

Message pipeline construction, tool calling flow, structured output parsing, streaming token buffering, context window management, and multi-turn conversation state.

Code Examples

Example: Typical Usage

// Replace with actual code example
void example() {
    // ...
}

Component Lifecycle in Detail

Each component instance goes through a well-defined lifecycle from instantiation to execution to cleanup. Understanding this helps with debugging state management issues.

Phase 1: Instantiation

Phase 2: Validation

Phase 3: Execution

Phase 4: History Recording

Component Registry & Plugin System

RAGFlow uses a dynamic registry pattern to load components. The system supports both built-in and custom plugins, enabling extensibility without modifying core code.

Registry Architecture

Extending with Custom Components

PatternUse CaseExample
Inline SubclassOne-off logic specific to a workflowDefine class in DSL loader; register at runtime
File-based CustomReusable across multiple workflowsmy_component.py in custom/ folder; imported by __init__.py
Versioned Plugin BundleDistributed to team; versioned releasesPyPI package with entry_points; Canvas.register_plugins(path) calls

Component Init Order

When Canvas.load() executes, components are instantiated in dependency order:

  1. DSL parsed into Graph object with component specs
  2. Topological sort on the DAG
  3. Each component class looked up in registry by type name
  4. __init__() called with merged config (DSL defaults + runtime overrides)
  5. validate_params() invokes schema validation; raises ValueError if invalid
  6. Component instance added to _components[component_id]; ready to execute