Python: [DRAFT] workflow composibility design#2366
Python: [DRAFT] workflow composibility design#2366moonbox3 wants to merge 8 commits intomicrosoft:mainfrom
Conversation
eavanvalkenburg
left a comment
There was a problem hiding this comment.
some very interesting things in here, I agree that option 2 makes the most sense, but I think the port idea might make some other scenario's easier and should be considered.
| analysis = ( | ||
| ConcurrentBuilder() | ||
| .participants([operation, compliance]) | ||
| .as_connection() |
There was a problem hiding this comment.
is this even needed? couldn't the next step just accept a unbuild workflowbuilder of any type? and then call as_connection itself, the function doesn't have params, so should be easy. Alternatively the prefix could be set in as_connection and then you can just connect.
There was a problem hiding this comment.
or even have prefix default to the name of the workflow, making it a bit easier to keep track of it.
| ) | ||
|
|
||
| builder = WorkflowBuilder() | ||
| analysis_handle = builder.connect(analysis, prefix="analysis") # merge + handle |
There was a problem hiding this comment.
so while I understand the connect verbiage, why not keep it a bit simpler and just add_workflow, which allows either a WorkflowConnection or a WorkflowBuilder (and that probably means also making sure that the workflowbuilders are all based on a single baseclass with the as_connection logic.
There was a problem hiding this comment.
I've now got a shared mixin-style surface (as_connection, add_workflow as the single verb) rather than forcing all high-level builders into a single inheritance base because:
- High-level builders already have distinct ctor shapes and internal state; pushing them under one base would introduce a breaking constructor contract or a leaky abstract layer we’d need to retrofit across Concurrent/Sequential/GroupChat/Magentic/Handoff.
- The behavior we need is tiny (expose .as_connection(prefix=...) and route through WorkflowBuilder), which we can provide uniformly via a mixin or thin delegations without reshaping the class hierarchy.
- add_workflow accepts WorkflowBuilder, Workflow, or WorkflowConnection; so callers get the simple API, while we keep existing builder implementations intact.
- This keeps public surface additive and avoids a sweeping inheritance refactor now; if we later find more shared behavior, we can introduce a common base with backward-compatible constructors.
| - Composition helpers use contracts to fail fast or select the right canned adapter. | ||
| - Pros: predictable type-safe bridges and better error messages. Cons: adds small surface area but aligns with existing adapter executors already used inside SequentialBuilder. | ||
|
|
||
| ## Option 4: Port-Based Interfaces and Extension Points |
There was a problem hiding this comment.
This would be a interesting feature as well, making it very explicit which input and outputs there are, also allowing hosted workflows to be instantiated with different endpoints for the different input ports.
| - Keep checkpointing, request/response handling, and observability semantics intact across composed graphs. | ||
|
|
||
| ## Current State | ||
| - High-level builders (ConcurrentBuilder, SequentialBuilder, group chat variants) emit a finished Workflow; the graph is immutable and cannot be extended directly. |
There was a problem hiding this comment.
I think we need to make sure that the responsibilities of our abstractions are clearly defined:
- A Workflow instance: built from a workflow builder and immutable
- A WorkflowBuilder instance: created by the users and mutable
This makes the composition story more streamlined and with less ambiguity. This also potentially aligns better with the declarative story: the declarative language is essentially a builder language.
There was a problem hiding this comment.
Adding a terminology section.
| - Type compatibility is enforced by WorkflowBuilder during validation, but only within a single builder instance; cross-workflow composition relies on developers hand-wiring compatible adapters. | ||
|
|
||
| ## Requirements | ||
| - Compose multiple workflows (built from any builder) as first-class nodes inside a parent graph. |
There was a problem hiding this comment.
Have we not achieved this via subworkflows?
There was a problem hiding this comment.
Or you mean the workflow must not be wrapped in an executor?
|
|
||
| ## Option 1: WorkflowExecutor-Centric Composition Helpers | ||
| - Add a fluent creation path for WorkflowExecutor to remove manual boilerplate: | ||
| - `Workflow.as_executor(id: str | None = None, *, allow_direct_output: bool = False, adapter: Callable[[Any], Any] | None = None) -> WorkflowExecutor` |
There was a problem hiding this comment.
This is just syntactic sugar for what we already have:
workflow_executor = WorkflowExecutor(workflow, id="abc", ...)There was a problem hiding this comment.
Agreed - this is syntactic sugar. The doc is exploring different approaches; we'll select what makes sense based on the full discussion.
|
Superseded by: #3296 |
Motivation and Context
Putting some thoughts and a POC together around workflow compatibility - being able to connect together two workflows, edges + high-level orchestration APIs, etc.
Description
Contribution Checklist