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).
📚 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 Choice | What Was Chosen | What Was Rejected | Why |
|---|---|---|---|
| Component model | Stateful objects with memory | Pure functional transforms | Stateful enables multi-turn LLM memory; functional is easier to parallelize but loses context |
| Parameter validation | Load-time schema checks | Runtime type coercion | Load-time catches errors early; runtime is more flexible but hides bugs |
| Registry mechanism | Factory functions (explicit) | Class import via reflection | Factories are safe; reflection risks circular imports and side effects |
| Output caching | Immutable cache per component | Re-compute on every lookup | Caching 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 / Function | File | Purpose |
|---|---|---|
ComponentBase | component/base.py | Abstract base with init, validate_params, run, to_dict; all components inherit |
ComponentParamBase | component/base.py | Nested class defining input/output schemas with types, defaults, descriptions |
LLM | component/llm.py | Calls chat model (OpenAI, Anthropic); supports structured output (JSON Schema) |
Agent | component/agent_with_tools.py | Extends LLM; adds tool-calling loop; LLM suggests tool, Agent calls it, loops until stop |
Switch | component/switch.py | Evaluates conditions; routes to multiple next-step branches; fan-out support |
Data Structures
| Type | Name | Description |
|---|---|---|
| dict | params | Configuration dict for component (e.g., {model: "gpt-4", temperature: 0.7}) |
| dict | _cache | Output cache; stores latest run output; reused by downstream components |
| dict | _inputs_history | All inputs ever passed to this component; for debugging and replay |
| list | schema.inputs | Parameter schema: [{name, type, default, description}] |
| list | schema.outputs | Output schema: [{name, type, description}] |
Interactions
| Direction | Partner | Interface | Description |
|---|---|---|---|
| → | Canvas | component.run(inputs) | Canvas passes resolved inputs; component executes and returns outputs |
| → | Tool Registry | tool_registry.get(name) | Agent component looks up and calls tools by name |
| → | Param Schema | schema.validate(params) | Canvas calls at load-time to validate parameter types and presence |
| ← | UI Builder | schema.to_html_form() | UI auto-generates form from param schema; users configure visually |
Configuration
| Knob / Parameter | Default | Description |
|---|---|---|
max_retries | 3 | Number of retry attempts on failure; exponential backoff applied |
exception_default_value | null | If all retries fail, return this value instead of propagating error |
exception_goto | null | If error occurs, jump to this next-step node (error handling branch) |
timeout_sec | 30 | Maximum 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 Case | What Happens | Where in Code | Risk Level |
|---|---|---|---|
| Empty input dict | Component receives no inputs; uses param defaults; may fail if required params missing | ComponentBase.run() | 🔴 High — component must handle gracefully or validate earlier |
| Component name collision | Two components have same ID (cpn_id); Canvas._outputs overwrites first | Canvas.load() | 🔴 High — Canvas validates unique IDs; error if duplicate detected |
| Circular component dependency | Component 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 timeout | LLM returns stream but drops connection mid-stream; partial output buffered | LLM.run() | 🔴 High — stream handler catches and propagates error; retry logic applies |
| Concurrent access | How it's handled | function_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
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
- Trigger: Canvas.load() encounters component spec in DSL with type="component_type"
- Registry lookup: ComponentRegistry.get("component_type") returns factory function
- Factory call: Factory(component_id, config_dict) instantiates component; stores config in self._config
- Outcome: Component object created but not yet validated; stored in Canvas._components[component_id]
Phase 2: Validation
- Trigger: Canvas.load() calls component.validate_params() after instantiation
- Schema check: For each parameter in component._schema, verify:
- Required params are present in self._config
- Parameter type matches schema (string, int, list, dict, etc.)
- Custom validators (min/max, regex, enum) pass
- Outcome: If valid, component is ready. If invalid, ValidationError raised; Canvas.load() fails; workflow not executable.
Phase 3: Execution
- Trigger: Canvas.execute() determines component has all dependencies resolved
- Input prep: Canvas resolves all variable references in parameters; builds inputs dict
- Run: component.run(inputs) called; component logic executes
- Output capture: Component returns output dict; Canvas stores in _outputs[component_id]
- Error handling: If exception raised:
- Check if retriable (network error, timeout, API rate limit)
- If retriable, sleep(backoff_ms) then call run() again (max 5 attempts)
- If not retriable or retries exhausted, use exception_default_value or exception_goto if configured
- If no fallback, exception propagates; workflow halts
Phase 4: History Recording
- Trigger: After component.run() completes (success or error)
- Record: Canvas appends execution step to _history list: {step_id, component_id, inputs, outputs, error, timestamp, duration_ms}
- Use case: Post-execution audit, debugging, compliance logging, performance analysis
- Cost: O(N) memory where N = total executed nodes; disable if memory constrained (set history_enabled=False in Canvas init)
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
- Centralized Registry: ComponentRegistry singleton maintains type → class mapping. Populated at runtime via imports and decorators.
- Lazy Loading: Components loaded on first DSL reference; avoids circular imports and unused overhead.
- Plugin Discovery: Custom components in agent/components/custom/ auto-discovered via __init__.py exports; decorated with @register_component("type_name").
- Fallback Logic: If component type not in registry at execution time, raises ComponentNotFound immediately. No silent defaults; explicit error handling required.
Extending with Custom Components
| Pattern | Use Case | Example |
|---|---|---|
| Inline Subclass | One-off logic specific to a workflow | Define class in DSL loader; register at runtime |
| File-based Custom | Reusable across multiple workflows | my_component.py in custom/ folder; imported by __init__.py |
| Versioned Plugin Bundle | Distributed to team; versioned releases | PyPI package with entry_points; Canvas.register_plugins(path) calls |
Component Init Order
When Canvas.load() executes, components are instantiated in dependency order:
- DSL parsed into Graph object with component specs
- Topological sort on the DAG
- Each component class looked up in registry by type name
- __init__() called with merged config (DSL defaults + runtime overrides)
- validate_params() invokes schema validation; raises ValueError if invalid
- Component instance added to _components[component_id]; ready to execute