"""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": {}, }