Add @Flow.model functional API#206
Open
NeejWeej wants to merge 13 commits into
Open
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #206 +/- ##
==========================================
- Coverage 95.37% 93.88% -1.49%
==========================================
Files 142 150 +8
Lines 11404 17011 +5607
Branches 620 1090 +470
==========================================
+ Hits 10876 15971 +5095
- Misses 399 820 +421
- Partials 129 220 +91 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
dddfb5b to
9f1755c
Compare
Signed-off-by: Nijat K <nijat.khanbabayev@gmail.com>
9f1755c to
a074f8f
Compare
Signed-off-by: Nijat K <nijat.khanbabayev@gmail.com>
4038d26 to
af73a7f
Compare
Signed-off-by: Nijat K <nijat.khanbabayev@gmail.com>
af73a7f to
6ff5409
Compare
Signed-off-by: Nijat K <nijat.khanbabayev@gmail.com>
Signed-off-by: Nijat K <nijat.khanbabayev@gmail.com>
7d4e83c to
40f3adb
Compare
Signed-off-by: Nijat K <nijat.khanbabayev@gmail.com>
fdd9b8a to
c193211
Compare
c193211 to
16aae1f
Compare
Signed-off-by: Nijat K <nijat.khanbabayev@gmail.com>
16896d0 to
7c620e2
Compare
Signed-off-by: Nijat K <nijat.khanbabayev@gmail.com>
Signed-off-by: Nijat K <nijat.khanbabayev@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR Summary:
@Flow.modelBranch:
nk/auto_deps_auto_callable_modelReplaces #171. Reopened from a personal fork.
This PR adds
@Flow.model, an authoring API that turns a typed Python functioninto a real
CallableModelfactory. The goal is to make common DAG stageseasier to write while keeping execution inside existing ccflow machinery:
CallableModel, evaluators, caches, dependency graphs, registry/Hydra loading,and serialization.
Core API
@Flow.modelsplits function parameters into two categories:model inputs and may be literals, defaults, or direct upstream
CallableModeldependencies.FromContext[T]. These areruntime inputs supplied by context,
.flow.compute(...), construction-timecontextual defaults, or
.flow.with_context(...).When a function returns a non-
ResultBasevalue, the generated model wraps it inGenericResult. ExplicitResultBasereturns are preserved.Dependency Wiring
Regular parameters can be bound directly to upstream models:
Only direct regular-parameter values are treated as upstream dependencies in
this first version. Containers such as
list,tuple,dict, andsetareordinary literal values;
@Flow.modeldoes not scan them for nested modeldependencies.
Generated
__deps__methods expose non-lazy direct upstream dependencies to theexisting graph evaluator.
Lazy[T]is supported for direct dependency thunkswhen a dependency should only be evaluated if user code calls it.
Context Rewrites
This PR adds
.flow.with_context(...)plus@Flow.context_transform.with_context(...)rewrites runtime context for one dependency edge withoutmutating the wrapped model. This supports fanout patterns where the same model is
evaluated against different contextual inputs in different branches.
Context bindings are stored as one ordered operation stream. Chained
with_context(...)calls preserve write order. Context transforms read from theoriginal ambient context, not from values written by earlier bindings in the
chain. Earlier field bindings overwritten by later field bindings do not run or
require inputs; patch transforms remain conservative because their output keys
can be dynamic.
Positional
with_context(...)arguments must be bound@Flow.context_transformresults that return mappings. Keyword field bindings may be static values or
bound field transforms. Callable keyword values are allowed when the target
contextual field type validates them, for example
FromContext[Callable[..., T]].Execution And Introspection Helpers
Every
CallableModelnow exposesmodel.flow.The public
.flowsurface is intentionally small:compute(...): ergonomic execution from a context object or contextual kwargs.with_context(...): edge-local context rewrites.inspect(...): structured debugging and introspection.inspect(...)returns aFlowInspectionobject:The top-level inspection fields are current-level only. They describe the model
or wrapper being inspected, not a flattened view of the whole dependency graph.
inputs: a dict from function input name toInputSpec, including type,required/default/value/source information.
context_inputs: declared contextual contract for the model or wrapped model.runtime_inputs: direct runtime inputs the current model or wrapper may readafter its own bindings.
required_inputs: required direct runtime inputs still unsatisfied bydefaults or bindings.
bound_inputs: concrete values already fixed on the current model or wrapper.dependencies: dependency edges discovered from direct generated-modelregular inputs.
context_inputsintentionally remains faithful to the declared model contract.For bound wrappers, use
runtime_inputs,required_inputs,bound_inputs, andinputson the inspection object to understand the effective caller-facingcontext after bindings.
inspect(...)can also take a proposed context object or contextual kwargs.Those values are used structurally: known direct inputs get values, and
dependency edges get projected context.
inspect(...)does not validate unusedruntime fields, does not report missing runtime fields as a separate check
object, and does not try to flatten graph-wide requirements. A strict debug-time
input checker is intentionally deferred until current-model versus graph-wide
semantics are explicit.
Dependency depth is controlled by one option:
dependencies="direct"lists immediate dependency edges.dependencies="none"leaves
dependenciesempty.dependencies="recursive"follows inspect-visibledependencies from constructed
@Flow.modelinputs andwith_context(...)wrappers.
This is not a full evaluator graph browser. A handwritten
CallableModelcanappear as a dependency target when it is bound to an
@Flow.modelregular input,but
inspect(...)does not expand that handwritten model's customCallableModel.__deps__implementation. That broader graph introspection is afollow-on feature.
compute()deliberately does not bind regular parameters. If a kwarg matches aregular parameter or model configuration field, it raises instead of silently
treating runtime context as model construction input.
Flow.call(auto_context=...)The PR also adds
Flow.call(auto_context=...)as a narrow opt-in for handwrittenCallableModel.__call__methods that want to declare context fields askeyword-only parameters.
This is not the main
@Flow.modelauthoring path. It does not addFromContext[...], dependency wiring, generated factories, or.flow.with_context(...)semantics by itself.Serialization
Importable module-level
@Flow.modelfunctions produce generated classes withstable module import paths, so JSON/config-style round trips can work across
processes when the defining module is importable.
Local, nested, and
__main__generated models are best-effort forpickle/cloudpickle object transport, not stable config artifacts. Their analyzed
function contract is serialized so restore does not need to re-run type-hint
resolution in the receiving process.
Generated model and
BoundModelpickle restore use portable validation datainstead of raw Pydantic state. This avoids fragile process-local generic classes
such as
GenericResult[int]leaking into pickle/Ray payloads.@Flow.context_transformbindings always store a serialized analyzed config.They do not rely on import-path detection, because during decoration the module
global usually still points at the undecorated function.
Cache And Graph Identity
Public
cache_key(...)remains structural by default.Generated and bound models also support effective identity for model
evaluations. Effective identity describes the parts of an invocation that affect
the result, so unused ambient
FlowContextfields do not split built-in cacheentries or graph nodes.
The built-in
MemoryCacheEvaluatoruses:Custom evaluators can use the same public API if they want generated-model-aware
keys:
The default remains structural:
Ordinary handwritten
CallableModelclasses continue to use structuralidentity. This is intentional: arbitrary
CallableModel.__call__implementations can inspect context in ways ccflow cannot infer safely.
Opaque evaluators also use structural identity, since they could access
arbitrary fields on the context that differ from the signature of a generated
model.
Unexpected errors while deriving effective identity propagate. The only
structural fallback is the explicit internal "effective key unavailable" path,
such as recursive effective identity.
Why Effective Identity Matters
The existing structural key can over-split cache entries when callers pass a
richer context than the model semantically uses. With structural context
identity, adding or changing an ambient field for one branch of a DAG can
invalidate cache reuse in another branch that does not use that field.
For ordinary handwritten models, ccflow cannot safely infer what Python code
uses. A normal
__call__implementation might inspecttype(context), callcontext.model_dump(), read subclass-only fields, or otherwise depend on thefull runtime context object.
@Flow.modelimproves this case because consumed contextual inputs are explicitvia
FromContext[...], so generated models can safely ignore unused ambientfields in effective cache and graph identity.
Validation And Error Behavior
The generated model remains a Pydantic model, but ccflow owns runtime binding
and coercion semantics.
The generated model's stored Pydantic fields use
SkipValidation[...]. This isimplemented in
_generated_field_annotation(...), which is used whencreate_model(...)builds the generatedCallableModelsubclass. The publicfactory signature still shows the user-facing annotations (
T,FromContext[T],Lazy[T]);SkipValidation[...]is only for the internalPydantic fields stored on the generated model instance.
That prevents Pydantic field validation from forcing registry resolution,
dependency handling, lazy handling, or contextual-default handling before
ccflow's generated-model validator can apply the correct rules.
The generated Pydantic field schema keeps useful type information when Pydantic
can build a schema for it. If Pydantic cannot build a schema for known schema
construction reasons, only the Pydantic field schema falls back to
Any;runtime coercion still uses the real annotation.
Validation is literal-first for regular parameter values. Serialized-looking
dependency dictionaries using
type_or_target_are only interpreted asdependencies after normal literal validation fails where that distinction is
ambiguous.
Unexpected errors from type hint resolution, type adapter construction, runtime
validation internals, and effective identity derivation propagate instead of
being masked by broad fallback paths.
Dependency evaluation preserves the original exception type and adds dependency
path context when the Python runtime supports exception notes.
Compatibility
The PR is additive:
CallableModelimplementations continue to work.Flow.callbehavior is preserved.cache_key(...)remains structural unlesseffective=Trueis explicitlyrequested.
CallableModelcache keys and graph keys remain structural.FlowContextis an open runtime carrier for generated models.context_type=...can still be used to validateFromContext[...]fields against an existing nominal context.
Test Coverage
The test suite covers:
with_context(...)field and patch transforms,model.flow.inspect(...)introspection,cache_key(..., effective=True)behavior,CallableModelcompatibility,identity.