Dependency Injection
In complex data flow and workflow orchestration, how to elegantly decouple "business logic" from "configuration data/runtime state" is a core challenge. Slyme adopts a functional-style design, providing a powerful dependency injection mechanism that allows Nodes to remain pure and only focus on business computation.
Slyme currently implements dependency injection at two levels:
- Build-time Injection: Parameter injection when using Scope to initialize Nodes.
- Runtime Injection: Auto evaluation (Auto Eval) when Nodes execute.
Below, we will deeply explore the specific implementation details of these two mechanisms.
1. Node Initialization Based on Scope
When building workflows, we typically define various configuration items at different levels (such as global configuration, module configuration, component configuration). Slyme allows us, when creating Nodes, to pass multiple dictionaries (called Scope), and the underlying layer automatically injects needed parameters into Nodes through function signature analysis.
1.1 Signature Analysis
During decorator application, Slyme uses inspect.signature and typing.get_type_hints to analyze function signatures and type hints. During this process, Slyme collects and merges Spec objects (containing default values, whether auto-evaluation is needed, etc.) for each build-time parameter, and records parameter names for later use.
1.2 Scope Resolution
When creating a Node (@node / @expression / @wrapper), it supports passing multiple positional arguments *scopes and keyword arguments **kwargs. The specific injection logic is completed by the resolve_arguments function:
- ChainMap Priority Chain: Slyme internally uses the efficient standard library data structure
collections.ChainMapto combine all Scopes. The combination order isChainMap(overrides, *reversed(scopes)). - Lookup Rules: This means when looking up a parameter, it follows the priority order of kwargs (explicit override) > last scope > ... > first scope. If the same key exists in multiple scopes, the later passed scope overrides the previous ones.
- Parameter Cleaning: After obtaining raw data through resolution, the
process_kwargsfunction performs strict validation based on thespecsgenerated in the previous step. It throws exceptions for unknown parameters (preventing typos), and fills in default values or callsdefault_factoryfor missing parameters according to theSpecdefinition.
2. Node Auto Evaluation (Auto Eval)
Static configuration is often insufficient. In many scenarios, parameters needed by Nodes are dynamic, such as depending on the previous step's computation result in Context, or being a lazily executed expression. Slyme provides auto evaluation (Auto Eval) mechanism, allowing Nodes to transparently obtain dynamic dependencies at runtime.
2.1 Declarative Evaluation Annotation
Developers don't need to manually call ctx.get() inside Nodes. They only need to use Auto (which is Annotated[T, Spec(auto_eval=True)] underneath) in function signatures to mark that the parameter supports auto evaluation.
2.2 Compile-time Preparation
To ensure runtime efficiency, Slyme doesn't scan all parameters every time a Node executes. Related work is moved forward to the prepare phase when Nodes transition from definition state (*Def) to execution state (*Exec):
- Separate Parameters: In
*Exec's__init__method,_prepare_evalis called. It checks whether values contain objects needing evaluation based on the parameter'sSpec. Parameters not needing evaluation are put intoraw_kwargsfor direct pre-binding, while dynamic parameters are put intoeval_kwargs. - Generate Evaluation Plan (EvaluationPlan): For
eval_kwargs, Slyme callsprepare_eval_planto generate an optimized evaluation plan. - PyTree Flatten and Batch Processing:
- Dependencies may be complex nested structures (such as dictionaries containing lists, lists containing
Refobjects). Slyme uses the underlyingCTX_EVAL_ENGINE(based on PyTree) to "flatten" nested structures into a one-dimensional list of leaf nodes. - Then, traversing these leaf nodes, it looks up corresponding evaluators in the
EVALUATOR_REGISTRYregistry (such asref_evaluatorforReftype,expression_evaluatorforExpression). - Grouping the same evaluators together generates a batch-based execution plan
EvaluationPlan. This design merges scattered data access, greatly optimizing performance.
- Dependencies may be complex nested structures (such as dictionaries containing lists, lists containing
TIP
The evaluation plan is generated across parameters. That is, if a Node has multiple parameters annotated with Auto, Slyme processes them together, summarizing all the same evaluation types for batch processing, greatly improving evaluation efficiency.
2.3 Runtime Execution
When *Exec is truly called via __call__(ctx), the final step of dependency injection begins:
- Execute Evaluation Plan: Call
execute_eval_plan(ctx, eval_plan). - Batch Extraction: The execution engine calls Evaluators in batches. For example, for all Ref objects,
ref_evaluatorcalls the underlying efficientctx.extract(refs)in one go to get values corresponding to all paths; for expressions, it passesctxfor batch computation. Note that when batch evaluating Refs, Context Hook is called only once (passing batched Refs and values), making it convenient for Hook developers to optimize performance. - Structure Restoration (Unflatten): After obtaining all computed leaf node values, reassemble them into the original dict/list structure according to the original nested structure.
- Transparent Execution: Finally, these dynamically computed dependencies are merged with static
raw_kwargsand passed to the developer-defined Node function. From the developer's perspective, they receive fully unpacked pure data.
2.4 Notes: Timing of Auto Eval and Higher-Order Nodes
WARNING
The evaluation object of Auto is the initially passed ctx parameter when the function is called. Evaluation happens before the Node function body truly executes.
If you are creating a higher-order Node (for example, inside a @node function you loop through and call other @nodes, returning a new ctx), please be sure to note: parameter values injected via Auto will not automatically update with the new ctx generated by your internal calls to other @nodes.
For example, if you are writing a training loop @node and need to get the latest loss after each step executes:
- Wrong Approach: Mark the
lossparameter asAuto. This way, you can only get the old data evaluated when entering the loop. - Correct Approach: Keep the
lossparameter as a normalRef, so you can manually callctx.get(loss)after getting the latestctxinside each loop body to dynamically get the latest value.
If you want to dynamically evaluate complex nested structures (similar to structures supported by Auto) inside a higher-order @node function, you can manually call the underlying evaluation API:
from slyme.node.eval import eval_tree, async_eval_tree
# Inside a higher-order @node's execution logic, manually evaluate complex nested structures
resolved_values = eval_tree(current_ctx, complex_ref_structure)