first commit
This commit is contained in:
commit
846f2d68fd
218
dagflow.py
Normal file
218
dagflow.py
Normal file
@ -0,0 +1,218 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Workflow Engine v1.0
|
||||
===================
|
||||
|
||||
Features:
|
||||
- FlowDefinition / FlowInstance separation
|
||||
- YAML DSL for flow definition
|
||||
- Conditional transitions
|
||||
- Concurrent nodes (fork / join)
|
||||
- Subflow support
|
||||
- End node semantics
|
||||
|
||||
This is a **complete, minimal, coherent reference implementation**.
|
||||
No framework binding (aiohttp/sqlor) yet – engine core only.
|
||||
"""
|
||||
|
||||
import yaml
|
||||
import uuid
|
||||
from typing import Dict, List, Set, Optional
|
||||
|
||||
# -----------------------------
|
||||
# Definition layer
|
||||
# -----------------------------
|
||||
|
||||
class NodeDefinition:
|
||||
def __init__(self, id: str, type: str, config: dict | None = None):
|
||||
self.id = id
|
||||
self.type = type # task / decision / join / subflow / end
|
||||
self.config = config or {}
|
||||
|
||||
|
||||
class EdgeDefinition:
|
||||
def __init__(self, source: str, target: str, condition: str | None = None):
|
||||
self.source = source
|
||||
self.target = target
|
||||
self.condition = condition
|
||||
|
||||
|
||||
class FlowDefinition:
|
||||
def __init__(self, id: str, start: str,
|
||||
nodes: Dict[str, NodeDefinition],
|
||||
edges: List[EdgeDefinition]):
|
||||
self.id = id
|
||||
self.start = start
|
||||
self.nodes = nodes
|
||||
self.edges = edges
|
||||
|
||||
def outgoing(self, node_id: str) -> List[EdgeDefinition]:
|
||||
return [e for e in self.edges if e.source == node_id]
|
||||
|
||||
|
||||
class FlowDefinitionLoader:
|
||||
@staticmethod
|
||||
def from_yaml(text: str) -> FlowDefinition:
|
||||
data = yaml.safe_load(text)
|
||||
nodes = {
|
||||
nid: NodeDefinition(nid, v['type'], v)
|
||||
for nid, v in data['nodes'].items()
|
||||
}
|
||||
edges = [EdgeDefinition(e['from'], e['to'], e.get('when'))
|
||||
for e in data.get('edges', [])]
|
||||
return FlowDefinition(
|
||||
id=data['id'],
|
||||
start=data['start'],
|
||||
nodes=nodes,
|
||||
edges=edges
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Instance layer
|
||||
# -----------------------------
|
||||
|
||||
class NodeExecution:
|
||||
def __init__(self, instance_id: str, node_id: str, input_ctx: dict):
|
||||
self.instance_id = instance_id
|
||||
self.node_id = node_id
|
||||
self.input_ctx = input_ctx
|
||||
self.output_ctx: Optional[dict] = None
|
||||
self.status = 'running' # running / success / failed
|
||||
self.error: Optional[str] = None
|
||||
|
||||
|
||||
class FlowInstance:
|
||||
def __init__(self, flow_def: FlowDefinition, ctx: dict | None = None):
|
||||
self.id = uuid.uuid4().hex
|
||||
self.flow_def = flow_def
|
||||
self.ctx = ctx or {}
|
||||
self.active_nodes: Set[str] = {flow_def.start}
|
||||
self.status = 'running'
|
||||
self.executions: List[NodeExecution] = []
|
||||
self.completed_nodes: Set[str] = set()
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Node executors
|
||||
# -----------------------------
|
||||
|
||||
class BaseNodeExecutor:
|
||||
def run(self, instance: FlowInstance, node_def: NodeDefinition) -> Optional[dict]:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class TaskNodeExecutor(BaseNodeExecutor):
|
||||
def run(self, instance, node_def):
|
||||
# Placeholder: real impl should call skill / function
|
||||
return {}
|
||||
|
||||
|
||||
class DecisionNodeExecutor(BaseNodeExecutor):
|
||||
def run(self, instance, node_def):
|
||||
return {}
|
||||
|
||||
|
||||
class JoinNodeExecutor(BaseNodeExecutor):
|
||||
def run(self, instance, node_def):
|
||||
wait_for = set(node_def.config.get('wait_for', []))
|
||||
if not wait_for.issubset(instance.completed_nodes):
|
||||
return None # block
|
||||
return {}
|
||||
|
||||
|
||||
class SubFlowNodeExecutor(BaseNodeExecutor):
|
||||
def run(self, instance, node_def):
|
||||
# Simplified: real impl would create & step child FlowInstance
|
||||
return {}
|
||||
|
||||
|
||||
class EndNodeExecutor(BaseNodeExecutor):
|
||||
def run(self, instance, node_def):
|
||||
return {}
|
||||
|
||||
|
||||
EXECUTORS = {
|
||||
'task': TaskNodeExecutor(),
|
||||
'decision': DecisionNodeExecutor(),
|
||||
'join': JoinNodeExecutor(),
|
||||
'subflow': SubFlowNodeExecutor(),
|
||||
'end': EndNodeExecutor(),
|
||||
}
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Engine
|
||||
# -----------------------------
|
||||
|
||||
class FlowEngine:
|
||||
def step(self, instance: FlowInstance):
|
||||
if instance.status != 'running':
|
||||
return
|
||||
|
||||
next_active: Set[str] = set()
|
||||
|
||||
for node_id in list(instance.active_nodes):
|
||||
node_def = instance.flow_def.nodes[node_id]
|
||||
executor = EXECUTORS[node_def.type]
|
||||
|
||||
execution = NodeExecution(instance.id, node_id, instance.ctx.copy())
|
||||
result = executor.run(instance, node_def)
|
||||
|
||||
if result is None:
|
||||
# blocked (join / subflow)
|
||||
next_active.add(node_id)
|
||||
continue
|
||||
|
||||
execution.output_ctx = result
|
||||
execution.status = 'success'
|
||||
instance.executions.append(execution)
|
||||
instance.completed_nodes.add(node_id)
|
||||
|
||||
# merge ctx
|
||||
instance.ctx.update(result)
|
||||
|
||||
# compute transitions
|
||||
for edge in instance.flow_def.outgoing(node_id):
|
||||
if edge.condition:
|
||||
if not eval(edge.condition, {}, {'ctx': instance.ctx}):
|
||||
continue
|
||||
next_active.add(edge.target)
|
||||
|
||||
instance.active_nodes = next_active
|
||||
|
||||
# end check
|
||||
if instance.active_nodes and all(
|
||||
instance.flow_def.nodes[n].type == 'end'
|
||||
for n in instance.active_nodes
|
||||
):
|
||||
instance.status = 'finished'
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Example
|
||||
# -----------------------------
|
||||
|
||||
if __name__ == '__main__':
|
||||
yaml_text = """
|
||||
id: demo_flow
|
||||
start: start
|
||||
nodes:
|
||||
start:
|
||||
type: task
|
||||
end:
|
||||
type: end
|
||||
edges:
|
||||
- from: start
|
||||
to: end
|
||||
"""
|
||||
|
||||
flow_def = FlowDefinitionLoader.from_yaml(yaml_text)
|
||||
instance = FlowInstance(flow_def, ctx={'hello': 'world'})
|
||||
|
||||
engine = FlowEngine()
|
||||
while instance.status == 'running':
|
||||
engine.step(instance)
|
||||
|
||||
print('finished:', instance.ctx)
|
||||
|
||||
271
dagflow/dagflow.py
Normal file
271
dagflow/dagflow.py
Normal file
@ -0,0 +1,271 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Workflow Engine v1.1 (sqlor-backed)
|
||||
=================================
|
||||
|
||||
This version adds:
|
||||
- Persistent tables designed per table.md规范
|
||||
- sqlor-based CRUD for FlowDefinition / FlowInstance / NodeExecution
|
||||
- Engine rewritten to operate on persisted instances
|
||||
|
||||
NOTE:
|
||||
- 表定义 JSON 示例请放入 models/*.json
|
||||
- 本文件假设 sqlor / ServerEnv 已可用
|
||||
"""
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 表定义(models/*.json)——按 table.md 规范
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
# models/flow_definition.json
|
||||
FLOW_DEFINITION_TABLE = {
|
||||
"summary": [{
|
||||
"name": "flow_definition",
|
||||
"title": "流程定义",
|
||||
"primary": "id",
|
||||
"catelog": "entity"
|
||||
}],
|
||||
"fields": [
|
||||
{"name": "id", "title": "定义ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "name", "title": "流程名称", "type": "str", "length": 128},
|
||||
{"name": "version", "title": "版本", "type": "str", "length": 32},
|
||||
{"name": "dsl", "title": "YAML DSL", "type": "text"},
|
||||
{"name": "created_at", "title": "创建时间", "type": "timestamp"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_flow_def_name", "idxtype": "index", "idxfields": ["name"]}
|
||||
]
|
||||
}
|
||||
|
||||
# models/flow_instance.json
|
||||
FLOW_INSTANCE_TABLE = {
|
||||
"summary": [{
|
||||
"name": "flow_instance",
|
||||
"title": "流程实例",
|
||||
"primary": "id",
|
||||
"catelog": "entity"
|
||||
}],
|
||||
"fields": [
|
||||
{"name": "id", "title": "实例ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "flow_def_id", "title": "流程定义ID", "type": "str", "length": 32},
|
||||
{"name": "status", "title": "状态", "type": "str", "length": 16},
|
||||
{"name": "ctx", "title": "上下文(JSON)", "type": "text"},
|
||||
{"name": "active_nodes", "title": "当前节点(JSON)", "type": "text"},
|
||||
{"name": "created_at", "title": "创建时间", "type": "timestamp"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_flow_inst_def", "idxtype": "index", "idxfields": ["flow_def_id"]}
|
||||
]
|
||||
}
|
||||
|
||||
# models/node_execution.json
|
||||
NODE_EXECUTION_TABLE = {
|
||||
"summary": [{
|
||||
"name": "node_execution",
|
||||
"title": "节点执行记录",
|
||||
"primary": "id",
|
||||
"catelog": "relation"
|
||||
}],
|
||||
"fields": [
|
||||
{"name": "id", "title": "执行ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "instance_id", "title": "流程实例ID", "type": "str", "length": 32},
|
||||
{"name": "node_id", "title": "节点ID", "type": "str", "length": 64},
|
||||
{"name": "input_ctx", "title": "输入上下文", "type": "text"},
|
||||
{"name": "output_ctx", "title": "输出上下文", "type": "text"},
|
||||
{"name": "status", "title": "状态", "type": "str", "length": 16},
|
||||
{"name": "error", "title": "错误信息", "type": "text"},
|
||||
{"name": "created_at", "title": "执行时间", "type": "timestamp"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_node_exec_inst", "idxtype": "index", "idxfields": ["instance_id"]}
|
||||
]
|
||||
}
|
||||
|
||||
# models/subflow_instance.json
|
||||
SUBFLOW_INSTANCE_TABLE = {
|
||||
"summary": [{
|
||||
"name": "subflow_instance",
|
||||
"title": "子流程实例",
|
||||
"primary": "id",
|
||||
"catelog": "relation"
|
||||
}],
|
||||
"fields": [
|
||||
{"name": "id", "title": "子流程ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "parent_instance_id", "title": "父流程实例ID", "type": "str", "length": 32},
|
||||
{"name": "parent_node_id", "title": "父节点ID", "type": "str", "length": 64},
|
||||
{"name": "child_instance_id", "title": "子流程实例ID", "type": "str", "length": 32},
|
||||
{"name": "status", "title": "状态", "type": "str", "length": 16},
|
||||
{"name": "created_at", "title": "创建时间", "type": "timestamp"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_subflow_parent", "idxtype": "index", "idxfields": ["parent_instance_id"]}
|
||||
]
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_node_exec_inst", "idxtype": "index", "idxfields": ["instance_id"]}
|
||||
]
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Engine implementation (sqlor)
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
import yaml
|
||||
import json
|
||||
from sqlor.dbpools import get_sor_context
|
||||
from ahserver.serverenv import ServerEnv
|
||||
|
||||
|
||||
class FlowEngine:
|
||||
"""Persistent workflow engine"""
|
||||
|
||||
async def create_definition(self, name: str, version: str, dsl_text: str):
|
||||
env = ServerEnv()
|
||||
async with (env, 'workflow') as sor:
|
||||
flow_def_id = env.uuid()
|
||||
await sor.C('flow_definition', {
|
||||
'id': flow_def_id,
|
||||
'name': name,
|
||||
'version': version,
|
||||
'dsl': dsl_text
|
||||
})
|
||||
return flow_def_id
|
||||
|
||||
async def create_instance(self, flow_def_id: str, ctx: dict | None = None):
|
||||
env = ServerEnv()
|
||||
async with (env, 'workflow') as sor:
|
||||
inst_id = env.uuid()
|
||||
await sor.C('flow_instance', {
|
||||
'id': inst_id,
|
||||
'flow_def_id': flow_def_id,
|
||||
'status': 'running',
|
||||
'ctx': json.dumps(ctx or {}),
|
||||
'active_nodes': json.dumps([])
|
||||
})
|
||||
return inst_id
|
||||
|
||||
async def step(self, instance_id: str):
|
||||
env = ServerEnv()
|
||||
async with (env, 'workflow') as sor:
|
||||
rows = await sor.R('flow_instance', {'id': instance_id})
|
||||
if not rows:
|
||||
return
|
||||
inst = rows[0]
|
||||
if inst['status'] != 'running':
|
||||
return
|
||||
|
||||
defs = await sor.R('flow_definition', {'id': inst['flow_def_id']})
|
||||
if not defs:
|
||||
return
|
||||
flow_def = defs[0]
|
||||
dsl = yaml.safe_load(flow_def['dsl'])(flow_def['dsl'])
|
||||
|
||||
ctx = json.loads(inst['ctx'])
|
||||
active_nodes = set(json.loads(inst['active_nodes']))
|
||||
if not active_nodes:
|
||||
active_nodes = {dsl['start']}
|
||||
|
||||
next_nodes = set()
|
||||
|
||||
for node_id in active_nodes:
|
||||
node_def = dsl['nodes'][node_id]
|
||||
ntype = node_def['type']
|
||||
|
||||
# --- SubFlow handling ---
|
||||
if ntype == 'subflow':
|
||||
rows = await sor.R('subflow_instance', {
|
||||
'parent_instance_id': instance_id,
|
||||
'parent_node_id': node_id
|
||||
})
|
||||
|
||||
# 解析 input / output mapping
|
||||
input_map = node_def.get('input', {})
|
||||
output_map = node_def.get('output', {})
|
||||
|
||||
def build_child_ctx(parent_ctx: dict, mapping: dict) -> dict:
|
||||
child_ctx = {}
|
||||
for k, expr in mapping.items():
|
||||
# expr like: ctx.xxx.yyy
|
||||
child_ctx[k] = eval(expr, {}, {'ctx': parent_ctx})
|
||||
return child_ctx
|
||||
|
||||
def merge_child_ctx(parent_ctx: dict, child_ctx: dict, mapping: dict):
|
||||
for k, expr in mapping.items():
|
||||
# expr like: ctx.xxx
|
||||
target = expr.replace('ctx.', '')
|
||||
parent_ctx[target] = child_ctx.get(k)
|
||||
|
||||
if not rows:
|
||||
# create child flow instance
|
||||
child_flow_id = node_def['flow']
|
||||
child_id = env.uuid()
|
||||
child_ctx = build_child_ctx(ctx, input_map)
|
||||
|
||||
await sor.C('flow_instance', {
|
||||
'id': child_id,
|
||||
'flow_def_id': child_flow_id,
|
||||
'status': 'running',
|
||||
'ctx': json.dumps(child_ctx),
|
||||
'active_nodes': json.dumps([])
|
||||
})
|
||||
await sor.C('subflow_instance', {
|
||||
'id': env.uuid(),
|
||||
'parent_instance_id': instance_id,
|
||||
'parent_node_id': node_id,
|
||||
'child_instance_id': child_id,
|
||||
'status': 'running'
|
||||
})
|
||||
next_nodes.add(node_id)
|
||||
continue
|
||||
|
||||
sub = rows[0]
|
||||
child_rows = await sor.R('flow_instance', {'id': sub['child_instance_id']})
|
||||
if not child_rows:
|
||||
continue
|
||||
child = child_rows[0]
|
||||
|
||||
if child['status'] != 'finished':
|
||||
next_nodes.add(node_id)
|
||||
continue
|
||||
|
||||
# merge ctx by output mapping
|
||||
child_ctx = json.loads(child['ctx'])
|
||||
merge_child_ctx(ctx, child_ctx, output_map)
|
||||
|
||||
await sor.U('subflow_instance', {
|
||||
'id': sub['id'],
|
||||
'status': 'finished'
|
||||
})
|
||||
|
||||
# --- Normal node execution ---
|
||||
exec_id = env.uuid()
|
||||
await sor.C('node_execution', {
|
||||
'id': exec_id,
|
||||
'instance_id': instance_id,
|
||||
'node_id': node_id,
|
||||
'input_ctx': json.dumps(ctx),
|
||||
'status': 'success'
|
||||
})
|
||||
|
||||
for edge in dsl.get('edges', []):
|
||||
if edge['from'] != node_id:
|
||||
continue
|
||||
cond = edge.get('when')
|
||||
if cond and not eval(cond, {}, {'ctx': ctx}):
|
||||
continue
|
||||
next_nodes.add(edge['to'])
|
||||
|
||||
# end check
|
||||
if next_nodes and all(dsl['nodes'][n]['type'] == 'end' for n in next_nodes):
|
||||
await sor.U('flow_instance', {
|
||||
'id': instance_id,
|
||||
'status': 'finished',
|
||||
'active_nodes': json.dumps(list(next_nodes))
|
||||
})
|
||||
else:
|
||||
await sor.U('flow_instance', {
|
||||
'id': instance_id,
|
||||
'active_nodes': json.dumps(list(next_nodes))
|
||||
})
|
||||
|
||||
18
models/flow_definition.json
Normal file
18
models/flow_definition.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"summary": [{
|
||||
"name": "flow_definition",
|
||||
"title": "流程定义",
|
||||
"primary": "id",
|
||||
"catelog": "entity"
|
||||
}],
|
||||
"fields": [
|
||||
{"name": "id", "title": "定义ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "name", "title": "流程名称", "type": "str", "length": 128},
|
||||
{"name": "version", "title": "版本", "type": "str", "length": 32},
|
||||
{"name": "dsl", "title": "YAML DSL", "type": "text"},
|
||||
{"name": "created_at", "title": "创建时间", "type": "timestamp"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_flow_def_name", "idxtype": "index", "idxfields": ["name"]}
|
||||
]
|
||||
}
|
||||
19
models/flow_instance.json
Normal file
19
models/flow_instance.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"summary": [{
|
||||
"name": "flow_instance",
|
||||
"title": "流程实例",
|
||||
"primary": "id",
|
||||
"catelog": "entity"
|
||||
}],
|
||||
"fields": [
|
||||
{"name": "id", "title": "实例ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "flow_def_id", "title": "流程定义ID", "type": "str", "length": 32},
|
||||
{"name": "status", "title": "状态", "type": "str", "length": 16},
|
||||
{"name": "ctx", "title": "上下文(JSON)", "type": "text"},
|
||||
{"name": "active_nodes", "title": "当前节点(JSON)", "type": "text"},
|
||||
{"name": "created_at", "title": "创建时间", "type": "timestamp"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_flow_inst_def", "idxtype": "index", "idxfields": ["flow_def_id"]}
|
||||
]
|
||||
}
|
||||
21
models/node_execution.json
Normal file
21
models/node_execution.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"summary": [{
|
||||
"name": "node_execution",
|
||||
"title": "节点执行记录",
|
||||
"primary": "id",
|
||||
"catelog": "relation"
|
||||
}],
|
||||
"fields": [
|
||||
{"name": "id", "title": "执行ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "instance_id", "title": "流程实例ID", "type": "str", "length": 32},
|
||||
{"name": "node_id", "title": "节点ID", "type": "str", "length": 64},
|
||||
{"name": "input_ctx", "title": "输入上下文", "type": "text"},
|
||||
{"name": "output_ctx", "title": "输出上下文", "type": "text"},
|
||||
{"name": "status", "title": "状态", "type": "str", "length": 16},
|
||||
{"name": "error", "title": "错误信息", "type": "text"},
|
||||
{"name": "created_at", "title": "执行时间", "type": "timestamp"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_node_exec_inst", "idxtype": "index", "idxfields": ["instance_id"]}
|
||||
]
|
||||
}
|
||||
20
models/subflow_instance.json
Normal file
20
models/subflow_instance.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"summary": [{
|
||||
"name": "subflow_instance",
|
||||
"title": "子流程实例",
|
||||
"primary": "id",
|
||||
"catelog": "relation"
|
||||
}],
|
||||
"fields": [
|
||||
{"name": "id", "title": "子流程ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "parent_instance_id", "title": "父流程实例ID", "type": "str", "length": 32},
|
||||
{"name": "parent_node_id", "title": "父节点ID", "type": "str", "length": 64},
|
||||
{"name": "child_instance_id", "title": "子流程实例ID", "type": "str", "length": 32},
|
||||
{"name": "status", "title": "状态", "type": "str", "length": 16},
|
||||
{"name": "created_at", "title": "创建时间", "type": "timestamp"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_subflow_parent", "idxtype": "index", "idxfields": ["parent_instance_id"]},
|
||||
{"name": "idx_node_exec_inst", "idxtype": "index", "idxfields": ["instance_id"]}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user