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 implements dependency injection at two levels:
- Build-time Injection: Passing
RefandExpressionreferences directly as keyword arguments to Node constructors, usingR(RefFactory) for concise syntax. - Runtime Injection: Auto evaluation (Auto Eval) when Nodes execute.
Below, we will deeply explore the specific implementation details of these two mechanisms.
1. Build-time Injection with RefFactory
When building workflows, you configure Nodes by passing Ref and Expression objects directly as keyword arguments. The recommended way to create Ref objects is via R (RefFactory):
from slyme.context import R
# R.input.articles creates a RefFactory that auto-converts to Ref("input.articles")
llm_api(responses=R.output.responses, prompts=format_article_prompts(articles=R.input.articles))RefFactory is resolved transparently to Ref by all Slyme APIs (Context methods, Node constructors, etc.) via the RefLike type alias.
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 Parameter Resolution
When creating a Node (@node / @expression / @wrapper), you pass keyword arguments **kwargs matching the function's parameter names. The process_kwargs function performs strict validation based on the specs generated in the previous step. It throws exceptions for unknown parameters (preventing typos), and fills in default values or calls default_factory for missing parameters according to the Spec definition.
Deprecated
Positional Scope dict injection (my_node({"param": value})) is deprecated since slyme 0.1.1 and will be removed in 0.2.0. Use keyword arguments directly with R.x.y.z instead.
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. - 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)