commits
tags
"""Migrerar det gamla nästlade `{steps}`-formatet till nod/kant-grafen som
grafmotorn (rpa/graph_engine.py) kör.
Gammalt: steps: [ {type, ...}, {type:"loop", steps:[...]}, {type:"if", branches:[{condition, steps:[]}], else:[...]} ]
Nytt: nodes: [{id, type, position, ...fält}], edges: [{id, source, target, sourceHandle, targetHandle}]
Handtag: vanlig nod in/out; `if` -> in + case0..caseN + else; `loop` -> in + each + done.
jump_to_label försvinner - föregångarens kant pekas om till etikett-noden.
"""
from __future__ import annotations
import uuid
_CONTAINER_KEYS = ("steps", "branches", "else")
def _uid() -> str:
return uuid.uuid4().hex[:12]
def _flat_node(step: dict, pos: dict | None) -> dict:
"""Nod med stegets platta fält (utan behållar-nycklar). `if` behåller
branches men bara deras condition (inte steps)."""
node = {k: v for k, v in step.items() if k not in _CONTAINER_KEYS}
node.setdefault("id", _uid())
node["position"] = pos or {"x": 0, "y": 0}
if step.get("type") == "if":
node["branches"] = [{"condition": b.get("condition", {})} for b in step.get("branches", [])]
return node
class _Builder:
def __init__(self, layout: dict | None):
self.nodes: list[dict] = []
self.edges: list[dict] = []
self.layout = layout or {}
self._y = 0
def _pos(self, step: dict) -> dict:
sid = step.get("id")
if sid and sid in self.layout:
return self.layout[sid]
self._y += 130
return {"x": 0, "y": self._y}
def _edge(self, src: str, src_h: str, tgt: str, tgt_h: str = "in") -> None:
self.edges.append({
"id": f"e-{_uid()}",
"source": src, "sourceHandle": src_h,
"target": tgt, "targetHandle": tgt_h,
})
def chain(self, steps: list) -> tuple[str | None, list[tuple[str, str]]]:
"""Bygger noder för en steglista. Returnerar (entry_id, open_ends) där
open_ends är [(node_id, handle), ...] som ska kopplas vidare."""
label_map: dict[str, str] = {}
prepared: list[dict] = []
for s in steps:
s = dict(s)
s.setdefault("id", _uid())
prepared.append(s)
if s.get("label"):
label_map[s["label"]] = s["id"]
entry: str | None = None
open_ends: list[tuple[str, str]] = []
for s in prepared:
if s.get("type") == "jump_to_label":
tgt = label_map.get(s.get("target", ""))
if tgt:
for nid, h in open_ends:
self._edge(nid, h, tgt)
open_ends = [] # framåtflödet stannar här
continue
b_entry, b_open = self._block(s)
if entry is None:
entry = b_entry
for nid, h in open_ends:
self._edge(nid, h, b_entry)
open_ends = b_open
return entry, open_ends
def _block(self, step: dict) -> tuple[str, list[tuple[str, str]]]:
t = step.get("type")
node = _flat_node(step, self._pos(step))
self.nodes.append(node)
nid = node["id"]
if t == "loop":
body_entry, body_open = self.chain(step.get("steps", []))
if body_entry:
self._edge(nid, "each", body_entry)
for bn, bh in body_open: # kroppens slut loopar tillbaka
self._edge(bn, bh, nid)
return nid, [(nid, "done")]
if t == "if":
open_ends: list[tuple[str, str]] = []
for i, br in enumerate(step.get("branches", [])):
b_entry, b_open = self.chain(br.get("steps", []))
if b_entry:
self._edge(nid, f"case{i}", b_entry)
open_ends.extend(b_open)
else:
open_ends.append((nid, f"case{i}"))
els = step.get("else")
if els is not None:
e_entry, e_open = self.chain(els)
if e_entry:
self._edge(nid, "else", e_entry)
open_ends.extend(e_open)
else:
open_ends.append((nid, "else"))
else:
# inget else i gamla modellen = "fortsätt" -> else-handtaget öppet
open_ends.append((nid, "else"))
return nid, open_ends
return nid, [(nid, "out")]
def steps_to_graph(steps: list, layout: dict | None = None) -> dict:
b = _Builder(layout)
entry, _open = b.chain(steps or [])
return {"nodes": b.nodes, "edges": b.edges, "start": entry}
def flow_to_graph(flow: dict) -> dict:
"""Ett helt flöde: behåll settings/inputs/input_sets, konvertera steps."""
if flow.get("nodes") is not None:
return flow # redan graf
g = steps_to_graph(flow.get("steps", []), flow.get("layout"))
return {
"format": "graph",
"nodes": g["nodes"],
"edges": g["edges"],
"start": g["start"],
"settings": flow.get("settings", {}),
"inputs": flow.get("inputs", []),
"input_sets": flow.get("input_sets", {}),
"layout": {},
}