Add @Flow.model functional API#232
Open
timkpaine wants to merge 22 commits into
Open
Conversation
Signed-off-by: Nijat K <nijat.khanbabayev@gmail.com>
Signed-off-by: Nijat K <nijat.khanbabayev@gmail.com>
Signed-off-by: Nijat K <nijat.khanbabayev@gmail.com>
Signed-off-by: Nijat K <nijat.khanbabayev@gmail.com>
Signed-off-by: Nijat K <nijat.khanbabayev@gmail.com>
Signed-off-by: Nijat K <nijat.khanbabayev@gmail.com>
Signed-off-by: Nijat K <nijat.khanbabayev@gmail.com>
Signed-off-by: Nijat K <nijat.khanbabayev@gmail.com>
Signed-off-by: Nijat K <nijat.khanbabayev@gmail.com>
…rs defining local classes Signed-off-by: Nijat K <nijat.khanbabayev@gmail.com>
Contributor
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #232 +/- ##
==========================================
- Coverage 94.19% 93.05% -1.14%
==========================================
Files 150 163 +13
Lines 12094 17987 +5893
Branches 665 1166 +501
==========================================
+ Hits 11392 16738 +5346
- Misses 570 1023 +453
- Partials 132 226 +94 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
`_build_compute_context` mixed argument validation, the explicit-context path, plain-CallableModel defaults, and generated-model handling across one long function with several exit branches. Extract the three paths into `_compute_context_from_explicit`, `_compute_context_for_plain_model`, and `_compute_context_for_generated_model`, leaving `_build_compute_context` as a thin dispatcher. Behavior-preserving refactor; no functional change. Verified against the full flow_model, flow_context, hydra, and evaluator suites. Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
In ccflow a bare string field value already resolves from the model registry, so `ccflow.compose.model_alias`, a bare-string alias, and a root-relative `/name` alias should all dereference to the same registered model instance. `model_alias` is a Hydra convenience for the existing bare-string convention, not a separate mechanism. Adds a small Hydra config wiring one registered source three equivalent ways and tests asserting all three resolve to the same instance (not a literal string) and compute identically. Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
…235) `_parse_annotation` only peeled the top-level `Annotated`, so `Optional[FromContext[int]]` (a Union on the outside) was silently misclassified as a regular parameter and every call failed with a misleading "cannot satisfy unbound regular parameter" error. Detect `FromContext`/`Lazy` markers nested inside a top-level Optional and define the two spellings precisely: - `FromContext[Optional[int]]`: contextual, required-in-context, value may be None. - `Optional[FromContext[int]]`: contextual, optional; absent -> bound to None (an implicit None default synthesized in `_analyze_flow_function`). `FromContext[Optional[int]] = None` is therefore equivalent to `Optional[FromContext[int]]`, and an explicit default still wins. Distinct required-ness yields distinct config/cache identities via the existing has_function_default/function_default identity terms. Reject nested `Lazy` (`Optional[Lazy[int]]`) and non-Optional unions carrying `FromContext` (e.g. `Union[FromContext[int], str]`) with clear messages. Adds focused regression tests covering all call shapes, the consistency equivalences, and the rejection cases. Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
…e=) (#237) Class-based CallableModel execution already accepts positional/string context shorthand via ContextBase's ordered `zip(model_fields, v)` mapping (as used by Hydra `+context=[...]`). Generated @Flow.model instances expose the open FlowContext bag as their runtime context type, which has no declared fields to zip against, so positional shorthand was silently dropped. When a generated model declares a `context_type`, `compute()` now validates non-mapping shorthand (list/tuple/str) through that declared type first, then forwards the named values into the FlowContext bag. Mapping and named-kwarg inputs keep their existing paths. Scope: this covers the `compute()` entry point only. The direct-call form (`model([...])`) is intentionally not supported, since `Flow.call` validates against FlowContext before the generated body runs; supporting it would require reverting the bag-of-types design. Adds `_declared_context_type_for_model` and focused tests for list/tuple/string shorthand, parity with named inputs, and that bag-only models are unaffected. Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
The dependency-graph builder and cache-key path route every node through `_effective_evaluation_key()`. For models that do not opt into effective identity (everything except generated @Flow.model / BoundModel), the result must stay byte-for-byte identical to the structural `cache_key()`. Pin that equivalence so future changes to the effective path cannot silently shift cache or graph identity for ordinary CallableModel graphs: - effective cache_key == structural cache_key for simple, chain, and diamond graphs; - the dependency graph (root_id, node keys, edges) built via the effective path equals an independently-computed structural graph; - shared diamond leaves still dedupe to one node; - `_build_dependency_graph` returns the structural root key. Test-only; no library changes. Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
) By default a declared `context_type` may now be an "omnibus" superset: every `FromContext[...]` parameter must exist on the context with a compatible type, but the context may carry extra fields the model does not consume. This matches the otherwise-permissive bag-of-types design and lets multiple models share one broad context. - `_validate_declared_context_type(..., strict=False)` keeps the missing-field and type-compatibility checks but only enforces the "every required context field is a FromContext param" bijection when `strict=True`. - Runtime: `_validate_declared_context_values` validates the consumed fields individually when the declared context has unconsumed required fields (subset mode); otherwise it constructs the whole declared context so its cross-field validators still run. No config/serialization change needed. - Thread `strict` through `flow_model` and document it on `Flow.model`. Updates the existing extra-required-field test to reflect the new default (allowed by default, rejected under strict=True) and adds focused tests for subset execution, strict rejection/acceptance, and the shared missing/type checks that apply in both modes. Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.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.