Spaces:
				
			
			
	
			
			
		Build error
		
	
	
	
			
			
	
	
	
	
		
		
		Build error
		
	Revert "Adding tests (#50)"
Browse filesThis reverts commit ba7246a4c8b941b76d158eef41fc4616b609524a.
- lynxkite-app/src/lynxkite/app/crdt.py +15 -80
 - lynxkite-app/tests/test_crdt.py +0 -72
 - lynxkite-app/tests/test_main.py +0 -77
 - lynxkite-app/web/src/apiTypes.ts +9 -9
 - lynxkite-core/src/lynxkite/core/executors/one_by_one.py +9 -16
 - lynxkite-core/src/lynxkite/core/ops.py +24 -33
 - lynxkite-core/src/lynxkite/core/workspace.py +6 -40
 - lynxkite-core/tests/test_ops.py +0 -89
 - lynxkite-core/tests/test_workspace.py +0 -115
 - lynxkite-graph-analytics/src/lynxkite_plugins/graph_analytics/lynxkite_ops.py +26 -49
 - lynxkite-graph-analytics/src/lynxkite_plugins/graph_analytics/networkx_ops.py +1 -1
 - lynxkite-graph-analytics/src/lynxkite_plugins/graph_analytics/pytorch_model_ops.py +5 -3
 - lynxkite-graph-analytics/tests/test_lynxkite_ops.py +0 -100
 - lynxkite-lynxscribe/src/lynxkite_plugins/lynxscribe/llm_ops.py +7 -7
 - lynxkite-lynxscribe/src/lynxkite_plugins/lynxscribe/lynxscribe_ops.py +8 -8
 - lynxkite-lynxscribe/tests/test_llm_ops.py +2 -2
 - lynxkite-pillow-example/src/lynxkite_plugins/pillow_example/__init__.py +1 -1
 
    	
        lynxkite-app/src/lynxkite/app/crdt.py
    CHANGED
    
    | 
         @@ -29,11 +29,7 @@ def ws_exception_handler(exception, log): 
     | 
|
| 29 | 
         | 
| 30 | 
         | 
| 31 | 
         
             
            class WebsocketServer(pycrdt_websocket.WebsocketServer):
         
     | 
| 32 | 
         
            -
                async def init_room(self, name 
     | 
| 33 | 
         
            -
                    """Initialize a room for the workspace with the given name.
         
     | 
| 34 | 
         
            -
             
     | 
| 35 | 
         
            -
                    The workspace is loaded from "crdt_data" if it exists there, or from "data", or a new workspace is created.
         
     | 
| 36 | 
         
            -
                    """
         
     | 
| 37 | 
         
             
                    path = CRDT_PATH / f"{name}.crdt"
         
     | 
| 38 | 
         
             
                    assert path.is_relative_to(CRDT_PATH)
         
     | 
| 39 | 
         
             
                    ystore = pycrdt_websocket.ystore.FileYStore(path)
         
     | 
| 
         @@ -53,8 +49,6 @@ class WebsocketServer(pycrdt_websocket.WebsocketServer): 
     | 
|
| 53 | 
         
             
                        ws["edges"] = pycrdt.Array()
         
     | 
| 54 | 
         
             
                    if "env" not in ws:
         
     | 
| 55 | 
         
             
                        ws["env"] = "unset"
         
     | 
| 56 | 
         
            -
                        # We have two possible sources of truth for the workspaces, the YStore and the JSON files.
         
     | 
| 57 | 
         
            -
                        # In case we didn't find the workspace in the YStore, we try to load it from the JSON files.
         
     | 
| 58 | 
         
             
                        try_to_load_workspace(ws, name)
         
     | 
| 59 | 
         
             
                    room = pycrdt_websocket.YRoom(
         
     | 
| 60 | 
         
             
                        ystore=ystore, ydoc=ydoc, exception_handler=ws_exception_handler
         
     | 
| 
         @@ -68,12 +62,6 @@ class WebsocketServer(pycrdt_websocket.WebsocketServer): 
     | 
|
| 68 | 
         
             
                    return room
         
     | 
| 69 | 
         | 
| 70 | 
         
             
                async def get_room(self, name: str) -> pycrdt_websocket.YRoom:
         
     | 
| 71 | 
         
            -
                    """Get a room by name.
         
     | 
| 72 | 
         
            -
             
     | 
| 73 | 
         
            -
                    This method overrides the parent get_room method. The original creates an empty room,
         
     | 
| 74 | 
         
            -
                    with no associated Ydoc. Instead, we want to initialize the the room with a Workspace
         
     | 
| 75 | 
         
            -
                    object.
         
     | 
| 76 | 
         
            -
                    """
         
     | 
| 77 | 
         
             
                    if name not in self.rooms:
         
     | 
| 78 | 
         
             
                        self.rooms[name] = await self.init_room(name)
         
     | 
| 79 | 
         
             
                    room = self.rooms[name]
         
     | 
| 
         @@ -84,7 +72,7 @@ class WebsocketServer(pycrdt_websocket.WebsocketServer): 
     | 
|
| 84 | 
         
             
            last_ws_input = None
         
     | 
| 85 | 
         | 
| 86 | 
         | 
| 87 | 
         
            -
            def clean_input(ws_pyd 
     | 
| 88 | 
         
             
                for node in ws_pyd.nodes:
         
     | 
| 89 | 
         
             
                    node.data.display = None
         
     | 
| 90 | 
         
             
                    node.data.error = None
         
     | 
| 
         @@ -95,43 +83,21 @@ def clean_input(ws_pyd: workspace.Workspace): 
     | 
|
| 95 | 
         
             
                            delattr(node, key)
         
     | 
| 96 | 
         | 
| 97 | 
         | 
| 98 | 
         
            -
            def crdt_update(
         
     | 
| 99 | 
         
            -
                crdt_obj: pycrdt.Map | pycrdt.Array,
         
     | 
| 100 | 
         
            -
                python_obj: dict | list,
         
     | 
| 101 | 
         
            -
                non_collaborative_fields: set[str] = set(),
         
     | 
| 102 | 
         
            -
            ):
         
     | 
| 103 | 
         
            -
                """Update a CRDT object to match a Python object.
         
     | 
| 104 | 
         
            -
             
     | 
| 105 | 
         
            -
                The types between the CRDT object and the Python object must match. If the Python object
         
     | 
| 106 | 
         
            -
                is a dict, the CRDT object must be a Map. If the Python object is a list, the CRDT object
         
     | 
| 107 | 
         
            -
                must be an Array.
         
     | 
| 108 | 
         
            -
             
     | 
| 109 | 
         
            -
                Args:
         
     | 
| 110 | 
         
            -
                    crdt_obj: The CRDT object, that will be updated to match the Python object.
         
     | 
| 111 | 
         
            -
                    python_obj: The Python object to update with.
         
     | 
| 112 | 
         
            -
                    non_collaborative_fields: List of fields to treat as a black box. Black boxes are
         
     | 
| 113 | 
         
            -
                    updated as a whole, instead of having a fine-grained data structure to edit
         
     | 
| 114 | 
         
            -
                    collaboratively. Useful for complex fields that contain auto-generated data or
         
     | 
| 115 | 
         
            -
                    metadata.
         
     | 
| 116 | 
         
            -
                    The default is an empty set.
         
     | 
| 117 | 
         
            -
             
     | 
| 118 | 
         
            -
                Raises:
         
     | 
| 119 | 
         
            -
                    ValueError: If the Python object provided is not a dict or list.
         
     | 
| 120 | 
         
            -
                """
         
     | 
| 121 | 
         
             
                if isinstance(python_obj, dict):
         
     | 
| 122 | 
         
             
                    for key, value in python_obj.items():
         
     | 
| 123 | 
         
            -
                        if key in  
     | 
| 124 | 
         
             
                            crdt_obj[key] = value
         
     | 
| 125 | 
         
             
                        elif isinstance(value, dict):
         
     | 
| 126 | 
         
             
                            if crdt_obj.get(key) is None:
         
     | 
| 127 | 
         
             
                                crdt_obj[key] = pycrdt.Map()
         
     | 
| 128 | 
         
            -
                            crdt_update(crdt_obj[key], value,  
     | 
| 129 | 
         
             
                        elif isinstance(value, list):
         
     | 
| 130 | 
         
             
                            if crdt_obj.get(key) is None:
         
     | 
| 131 | 
         
             
                                crdt_obj[key] = pycrdt.Array()
         
     | 
| 132 | 
         
            -
                            crdt_update(crdt_obj[key], value,  
     | 
| 133 | 
         
             
                        elif isinstance(value, enum.Enum):
         
     | 
| 134 | 
         
            -
                            crdt_obj[key] = str(value 
     | 
| 135 | 
         
             
                        else:
         
     | 
| 136 | 
         
             
                            crdt_obj[key] = value
         
     | 
| 137 | 
         
             
                elif isinstance(python_obj, list):
         
     | 
| 
         @@ -139,14 +105,12 @@ def crdt_update( 
     | 
|
| 139 | 
         
             
                        if isinstance(value, dict):
         
     | 
| 140 | 
         
             
                            if i >= len(crdt_obj):
         
     | 
| 141 | 
         
             
                                crdt_obj.append(pycrdt.Map())
         
     | 
| 142 | 
         
            -
                            crdt_update(crdt_obj[i], value,  
     | 
| 143 | 
         
             
                        elif isinstance(value, list):
         
     | 
| 144 | 
         
             
                            if i >= len(crdt_obj):
         
     | 
| 145 | 
         
             
                                crdt_obj.append(pycrdt.Array())
         
     | 
| 146 | 
         
            -
                            crdt_update(crdt_obj[i], value,  
     | 
| 147 | 
         
             
                        else:
         
     | 
| 148 | 
         
            -
                            if isinstance(value, enum.Enum):
         
     | 
| 149 | 
         
            -
                                value = str(value.value)
         
     | 
| 150 | 
         
             
                            if i >= len(crdt_obj):
         
     | 
| 151 | 
         
             
                                crdt_obj.append(value)
         
     | 
| 152 | 
         
             
                            else:
         
     | 
| 
         @@ -155,34 +119,18 @@ def crdt_update( 
     | 
|
| 155 | 
         
             
                    raise ValueError("Invalid type:", python_obj)
         
     | 
| 156 | 
         | 
| 157 | 
         | 
| 158 | 
         
            -
            def try_to_load_workspace(ws 
     | 
| 159 | 
         
            -
                """Load the workspace `name`, if it exists, and update the `ws` CRDT object to match its contents.
         
     | 
| 160 | 
         
            -
             
     | 
| 161 | 
         
            -
                Args:
         
     | 
| 162 | 
         
            -
                    ws: CRDT object to udpate with the workspace contents.
         
     | 
| 163 | 
         
            -
                    name: Name of the workspace to load.
         
     | 
| 164 | 
         
            -
                """
         
     | 
| 165 | 
         
             
                json_path = f"data/{name}"
         
     | 
| 166 | 
         
             
                if os.path.exists(json_path):
         
     | 
| 167 | 
         
             
                    ws_pyd = workspace.load(json_path)
         
     | 
| 168 | 
         
            -
                     
     | 
| 169 | 
         
            -
                    # dictionary that is meant to change as a whole.
         
     | 
| 170 | 
         
            -
                    crdt_update(ws, ws_pyd.model_dump(), non_collaborative_fields={"display"})
         
     | 
| 171 | 
         | 
| 172 | 
         | 
| 173 | 
         
             
            last_known_versions = {}
         
     | 
| 174 | 
         
             
            delayed_executions = {}
         
     | 
| 175 | 
         | 
| 176 | 
         | 
| 177 | 
         
            -
            async def workspace_changed(name 
     | 
| 178 | 
         
            -
                """Callback to react to changes in the workspace.
         
     | 
| 179 | 
         
            -
             
     | 
| 180 | 
         
            -
             
     | 
| 181 | 
         
            -
                Args:
         
     | 
| 182 | 
         
            -
                    name: Name of the workspace.
         
     | 
| 183 | 
         
            -
                    changes: Changes performed to the workspace.
         
     | 
| 184 | 
         
            -
                    ws_crdt: CRDT object representing the workspace.
         
     | 
| 185 | 
         
            -
                """
         
     | 
| 186 | 
         
             
                ws_pyd = workspace.Workspace.model_validate(ws_crdt.to_py())
         
     | 
| 187 | 
         
             
                # Do not trigger execution for superficial changes.
         
     | 
| 188 | 
         
             
                # This is a quick solution until we build proper caching.
         
     | 
| 
         @@ -206,35 +154,22 @@ async def workspace_changed(name: str, changes: pycrdt.MapEvent, ws_crdt: pycrdt 
     | 
|
| 206 | 
         
             
                    await execute(name, ws_crdt, ws_pyd)
         
     | 
| 207 | 
         | 
| 208 | 
         | 
| 209 | 
         
            -
            async def execute(
         
     | 
| 210 | 
         
            -
                name: str, ws_crdt: pycrdt.Map, ws_pyd: workspace.Workspace, delay: int = 0
         
     | 
| 211 | 
         
            -
            ):
         
     | 
| 212 | 
         
            -
                """Execute the workspace and update the CRDT object with the results.
         
     | 
| 213 | 
         
            -
             
     | 
| 214 | 
         
            -
                Args:
         
     | 
| 215 | 
         
            -
                    name: Name of the workspace.
         
     | 
| 216 | 
         
            -
                    ws_crdt: CRDT object representing the workspace.
         
     | 
| 217 | 
         
            -
                    ws_pyd: Workspace object to execute.
         
     | 
| 218 | 
         
            -
                    delay: Wait time before executing the workspace. The default is 0.
         
     | 
| 219 | 
         
            -
                """
         
     | 
| 220 | 
         
             
                if delay:
         
     | 
| 221 | 
         
             
                    try:
         
     | 
| 222 | 
         
             
                        await asyncio.sleep(delay)
         
     | 
| 223 | 
         
             
                    except asyncio.CancelledError:
         
     | 
| 224 | 
         
             
                        return
         
     | 
| 225 | 
         
             
                path = DATA_PATH / name
         
     | 
| 226 | 
         
            -
                assert path.is_relative_to(DATA_PATH) 
     | 
| 227 | 
         
            -
                # Save user changes before executing, in case the execution fails.
         
     | 
| 228 | 
         
             
                workspace.save(ws_pyd, path)
         
     | 
| 229 | 
         
             
                await workspace.execute(ws_pyd)
         
     | 
| 230 | 
         
             
                workspace.save(ws_pyd, path)
         
     | 
| 231 | 
         
            -
                # Execution happened on the Python object, we need to replicate
         
     | 
| 232 | 
         
            -
                # the results to the CRDT object.
         
     | 
| 233 | 
         
             
                with ws_crdt.doc.transaction():
         
     | 
| 234 | 
         
             
                    for nc, np in zip(ws_crdt["nodes"], ws_pyd.nodes):
         
     | 
| 235 | 
         
             
                        if "data" not in nc:
         
     | 
| 236 | 
         
             
                            nc["data"] = pycrdt.Map()
         
     | 
| 237 | 
         
            -
                        # Display is added as  
     | 
| 238 | 
         
             
                        nc["data"]["display"] = np.data.display
         
     | 
| 239 | 
         
             
                        nc["data"]["error"] = np.data.error
         
     | 
| 240 | 
         | 
| 
         | 
|
| 29 | 
         | 
| 30 | 
         | 
| 31 | 
         
             
            class WebsocketServer(pycrdt_websocket.WebsocketServer):
         
     | 
| 32 | 
         
            +
                async def init_room(self, name):
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 33 | 
         
             
                    path = CRDT_PATH / f"{name}.crdt"
         
     | 
| 34 | 
         
             
                    assert path.is_relative_to(CRDT_PATH)
         
     | 
| 35 | 
         
             
                    ystore = pycrdt_websocket.ystore.FileYStore(path)
         
     | 
| 
         | 
|
| 49 | 
         
             
                        ws["edges"] = pycrdt.Array()
         
     | 
| 50 | 
         
             
                    if "env" not in ws:
         
     | 
| 51 | 
         
             
                        ws["env"] = "unset"
         
     | 
| 
         | 
|
| 
         | 
|
| 52 | 
         
             
                        try_to_load_workspace(ws, name)
         
     | 
| 53 | 
         
             
                    room = pycrdt_websocket.YRoom(
         
     | 
| 54 | 
         
             
                        ystore=ystore, ydoc=ydoc, exception_handler=ws_exception_handler
         
     | 
| 
         | 
|
| 62 | 
         
             
                    return room
         
     | 
| 63 | 
         | 
| 64 | 
         
             
                async def get_room(self, name: str) -> pycrdt_websocket.YRoom:
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 65 | 
         
             
                    if name not in self.rooms:
         
     | 
| 66 | 
         
             
                        self.rooms[name] = await self.init_room(name)
         
     | 
| 67 | 
         
             
                    room = self.rooms[name]
         
     | 
| 
         | 
|
| 72 | 
         
             
            last_ws_input = None
         
     | 
| 73 | 
         | 
| 74 | 
         | 
| 75 | 
         
            +
            def clean_input(ws_pyd):
         
     | 
| 76 | 
         
             
                for node in ws_pyd.nodes:
         
     | 
| 77 | 
         
             
                    node.data.display = None
         
     | 
| 78 | 
         
             
                    node.data.error = None
         
     | 
| 
         | 
|
| 83 | 
         
             
                            delattr(node, key)
         
     | 
| 84 | 
         | 
| 85 | 
         | 
| 86 | 
         
            +
            def crdt_update(crdt_obj, python_obj, boxes=set()):
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 87 | 
         
             
                if isinstance(python_obj, dict):
         
     | 
| 88 | 
         
             
                    for key, value in python_obj.items():
         
     | 
| 89 | 
         
            +
                        if key in boxes:
         
     | 
| 90 | 
         
             
                            crdt_obj[key] = value
         
     | 
| 91 | 
         
             
                        elif isinstance(value, dict):
         
     | 
| 92 | 
         
             
                            if crdt_obj.get(key) is None:
         
     | 
| 93 | 
         
             
                                crdt_obj[key] = pycrdt.Map()
         
     | 
| 94 | 
         
            +
                            crdt_update(crdt_obj[key], value, boxes)
         
     | 
| 95 | 
         
             
                        elif isinstance(value, list):
         
     | 
| 96 | 
         
             
                            if crdt_obj.get(key) is None:
         
     | 
| 97 | 
         
             
                                crdt_obj[key] = pycrdt.Array()
         
     | 
| 98 | 
         
            +
                            crdt_update(crdt_obj[key], value, boxes)
         
     | 
| 99 | 
         
             
                        elif isinstance(value, enum.Enum):
         
     | 
| 100 | 
         
            +
                            crdt_obj[key] = str(value)
         
     | 
| 101 | 
         
             
                        else:
         
     | 
| 102 | 
         
             
                            crdt_obj[key] = value
         
     | 
| 103 | 
         
             
                elif isinstance(python_obj, list):
         
     | 
| 
         | 
|
| 105 | 
         
             
                        if isinstance(value, dict):
         
     | 
| 106 | 
         
             
                            if i >= len(crdt_obj):
         
     | 
| 107 | 
         
             
                                crdt_obj.append(pycrdt.Map())
         
     | 
| 108 | 
         
            +
                            crdt_update(crdt_obj[i], value, boxes)
         
     | 
| 109 | 
         
             
                        elif isinstance(value, list):
         
     | 
| 110 | 
         
             
                            if i >= len(crdt_obj):
         
     | 
| 111 | 
         
             
                                crdt_obj.append(pycrdt.Array())
         
     | 
| 112 | 
         
            +
                            crdt_update(crdt_obj[i], value, boxes)
         
     | 
| 113 | 
         
             
                        else:
         
     | 
| 
         | 
|
| 
         | 
|
| 114 | 
         
             
                            if i >= len(crdt_obj):
         
     | 
| 115 | 
         
             
                                crdt_obj.append(value)
         
     | 
| 116 | 
         
             
                            else:
         
     | 
| 
         | 
|
| 119 | 
         
             
                    raise ValueError("Invalid type:", python_obj)
         
     | 
| 120 | 
         | 
| 121 | 
         | 
| 122 | 
         
            +
            def try_to_load_workspace(ws, name):
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 123 | 
         
             
                json_path = f"data/{name}"
         
     | 
| 124 | 
         
             
                if os.path.exists(json_path):
         
     | 
| 125 | 
         
             
                    ws_pyd = workspace.load(json_path)
         
     | 
| 126 | 
         
            +
                    crdt_update(ws, ws_pyd.model_dump(), boxes={"display"})
         
     | 
| 
         | 
|
| 
         | 
|
| 127 | 
         | 
| 128 | 
         | 
| 129 | 
         
             
            last_known_versions = {}
         
     | 
| 130 | 
         
             
            delayed_executions = {}
         
     | 
| 131 | 
         | 
| 132 | 
         | 
| 133 | 
         
            +
            async def workspace_changed(name, changes, ws_crdt):
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 134 | 
         
             
                ws_pyd = workspace.Workspace.model_validate(ws_crdt.to_py())
         
     | 
| 135 | 
         
             
                # Do not trigger execution for superficial changes.
         
     | 
| 136 | 
         
             
                # This is a quick solution until we build proper caching.
         
     | 
| 
         | 
|
| 154 | 
         
             
                    await execute(name, ws_crdt, ws_pyd)
         
     | 
| 155 | 
         | 
| 156 | 
         | 
| 157 | 
         
            +
            async def execute(name, ws_crdt, ws_pyd, delay=0):
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 158 | 
         
             
                if delay:
         
     | 
| 159 | 
         
             
                    try:
         
     | 
| 160 | 
         
             
                        await asyncio.sleep(delay)
         
     | 
| 161 | 
         
             
                    except asyncio.CancelledError:
         
     | 
| 162 | 
         
             
                        return
         
     | 
| 163 | 
         
             
                path = DATA_PATH / name
         
     | 
| 164 | 
         
            +
                assert path.is_relative_to(DATA_PATH)
         
     | 
| 
         | 
|
| 165 | 
         
             
                workspace.save(ws_pyd, path)
         
     | 
| 166 | 
         
             
                await workspace.execute(ws_pyd)
         
     | 
| 167 | 
         
             
                workspace.save(ws_pyd, path)
         
     | 
| 
         | 
|
| 
         | 
|
| 168 | 
         
             
                with ws_crdt.doc.transaction():
         
     | 
| 169 | 
         
             
                    for nc, np in zip(ws_crdt["nodes"], ws_pyd.nodes):
         
     | 
| 170 | 
         
             
                        if "data" not in nc:
         
     | 
| 171 | 
         
             
                            nc["data"] = pycrdt.Map()
         
     | 
| 172 | 
         
            +
                        # Display is added as an opaque Box.
         
     | 
| 173 | 
         
             
                        nc["data"]["display"] = np.data.display
         
     | 
| 174 | 
         
             
                        nc["data"]["error"] = np.data.error
         
     | 
| 175 | 
         | 
    	
        lynxkite-app/tests/test_crdt.py
    DELETED
    
    | 
         @@ -1,72 +0,0 @@ 
     | 
|
| 1 | 
         
            -
            from enum import Enum
         
     | 
| 2 | 
         
            -
            import pycrdt
         
     | 
| 3 | 
         
            -
            import pytest
         
     | 
| 4 | 
         
            -
            from lynxkite.app.crdt import crdt_update
         
     | 
| 5 | 
         
            -
             
     | 
| 6 | 
         
            -
             
     | 
| 7 | 
         
            -
            @pytest.fixture
         
     | 
| 8 | 
         
            -
            def empty_dict_workspace():
         
     | 
| 9 | 
         
            -
                ydoc = pycrdt.Doc()
         
     | 
| 10 | 
         
            -
                ydoc["workspace"] = ws = pycrdt.Map()
         
     | 
| 11 | 
         
            -
                yield ws
         
     | 
| 12 | 
         
            -
             
     | 
| 13 | 
         
            -
             
     | 
| 14 | 
         
            -
            @pytest.fixture
         
     | 
| 15 | 
         
            -
            def empty_list_workspace():
         
     | 
| 16 | 
         
            -
                ydoc = pycrdt.Doc()
         
     | 
| 17 | 
         
            -
                ydoc["workspace"] = ws = pycrdt.Array()
         
     | 
| 18 | 
         
            -
                yield ws
         
     | 
| 19 | 
         
            -
             
     | 
| 20 | 
         
            -
             
     | 
| 21 | 
         
            -
            class MyEnum(Enum):
         
     | 
| 22 | 
         
            -
                VALUE = 1
         
     | 
| 23 | 
         
            -
             
     | 
| 24 | 
         
            -
             
     | 
| 25 | 
         
            -
            @pytest.mark.parametrize(
         
     | 
| 26 | 
         
            -
                "python_obj,expected",
         
     | 
| 27 | 
         
            -
                [
         
     | 
| 28 | 
         
            -
                    (
         
     | 
| 29 | 
         
            -
                        {
         
     | 
| 30 | 
         
            -
                            "key1": "value1",
         
     | 
| 31 | 
         
            -
                            "key2": {
         
     | 
| 32 | 
         
            -
                                "nested_key1": "nested_value1",
         
     | 
| 33 | 
         
            -
                                "nested_key2": ["nested_value2"],
         
     | 
| 34 | 
         
            -
                                "nested_key3": MyEnum.VALUE,
         
     | 
| 35 | 
         
            -
                            },
         
     | 
| 36 | 
         
            -
                        },
         
     | 
| 37 | 
         
            -
                        {
         
     | 
| 38 | 
         
            -
                            "key1": "value1",
         
     | 
| 39 | 
         
            -
                            "key2": {
         
     | 
| 40 | 
         
            -
                                "nested_key1": "nested_value1",
         
     | 
| 41 | 
         
            -
                                "nested_key2": ["nested_value2"],
         
     | 
| 42 | 
         
            -
                                "nested_key3": "1",
         
     | 
| 43 | 
         
            -
                            },
         
     | 
| 44 | 
         
            -
                        },
         
     | 
| 45 | 
         
            -
                    )
         
     | 
| 46 | 
         
            -
                ],
         
     | 
| 47 | 
         
            -
            )
         
     | 
| 48 | 
         
            -
            def test_crdt_update_with_dict(empty_dict_workspace, python_obj, expected):
         
     | 
| 49 | 
         
            -
                crdt_update(empty_dict_workspace, python_obj)
         
     | 
| 50 | 
         
            -
                assert empty_dict_workspace.to_py() == expected
         
     | 
| 51 | 
         
            -
             
     | 
| 52 | 
         
            -
             
     | 
| 53 | 
         
            -
            @pytest.mark.parametrize(
         
     | 
| 54 | 
         
            -
                "python_obj,expected",
         
     | 
| 55 | 
         
            -
                [
         
     | 
| 56 | 
         
            -
                    (
         
     | 
| 57 | 
         
            -
                        [
         
     | 
| 58 | 
         
            -
                            "value1",
         
     | 
| 59 | 
         
            -
                            {"nested_key1": "nested_value1", "nested_key2": ["nested_value2"]},
         
     | 
| 60 | 
         
            -
                            MyEnum.VALUE,
         
     | 
| 61 | 
         
            -
                        ],
         
     | 
| 62 | 
         
            -
                        [
         
     | 
| 63 | 
         
            -
                            "value1",
         
     | 
| 64 | 
         
            -
                            {"nested_key1": "nested_value1", "nested_key2": ["nested_value2"]},
         
     | 
| 65 | 
         
            -
                            "1",
         
     | 
| 66 | 
         
            -
                        ],
         
     | 
| 67 | 
         
            -
                    ),
         
     | 
| 68 | 
         
            -
                ],
         
     | 
| 69 | 
         
            -
            )
         
     | 
| 70 | 
         
            -
            def test_crdt_update_with_list(empty_list_workspace, python_obj, expected):
         
     | 
| 71 | 
         
            -
                crdt_update(empty_list_workspace, python_obj)
         
     | 
| 72 | 
         
            -
                assert empty_list_workspace.to_py() == expected
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
    	
        lynxkite-app/tests/test_main.py
    DELETED
    
    | 
         @@ -1,77 +0,0 @@ 
     | 
|
| 1 | 
         
            -
            import uuid
         
     | 
| 2 | 
         
            -
            from fastapi.testclient import TestClient
         
     | 
| 3 | 
         
            -
            from lynxkite.app.main import app, detect_plugins, DATA_PATH
         
     | 
| 4 | 
         
            -
            import os
         
     | 
| 5 | 
         
            -
             
     | 
| 6 | 
         
            -
             
     | 
| 7 | 
         
            -
            client = TestClient(app)
         
     | 
| 8 | 
         
            -
             
     | 
| 9 | 
         
            -
             
     | 
| 10 | 
         
            -
            def test_detect_plugins_with_plugins():
         
     | 
| 11 | 
         
            -
                # This test assumes that these plugins are installed as part of the testing process.
         
     | 
| 12 | 
         
            -
                plugins = detect_plugins()
         
     | 
| 13 | 
         
            -
                assert all(
         
     | 
| 14 | 
         
            -
                    plugin in plugins.keys()
         
     | 
| 15 | 
         
            -
                    for plugin in [
         
     | 
| 16 | 
         
            -
                        "lynxkite_plugins.graph_analytics",
         
     | 
| 17 | 
         
            -
                        "lynxkite_plugins.lynxscribe",
         
     | 
| 18 | 
         
            -
                        "lynxkite_plugins.pillow_example",
         
     | 
| 19 | 
         
            -
                    ]
         
     | 
| 20 | 
         
            -
                )
         
     | 
| 21 | 
         
            -
             
     | 
| 22 | 
         
            -
             
     | 
| 23 | 
         
            -
            def test_get_catalog():
         
     | 
| 24 | 
         
            -
                response = client.get("/api/catalog")
         
     | 
| 25 | 
         
            -
                assert response.status_code == 200
         
     | 
| 26 | 
         
            -
             
     | 
| 27 | 
         
            -
             
     | 
| 28 | 
         
            -
            def test_save_and_load():
         
     | 
| 29 | 
         
            -
                save_request = {
         
     | 
| 30 | 
         
            -
                    "path": "test",
         
     | 
| 31 | 
         
            -
                    "ws": {
         
     | 
| 32 | 
         
            -
                        "env": "test",
         
     | 
| 33 | 
         
            -
                        "nodes": [
         
     | 
| 34 | 
         
            -
                            {
         
     | 
| 35 | 
         
            -
                                "id": "Node_1",
         
     | 
| 36 | 
         
            -
                                "type": "basic",
         
     | 
| 37 | 
         
            -
                                "data": {
         
     | 
| 38 | 
         
            -
                                    "display": None,
         
     | 
| 39 | 
         
            -
                                    "error": "Unknown operation.",
         
     | 
| 40 | 
         
            -
                                    "title": "Test node",
         
     | 
| 41 | 
         
            -
                                    "params": {"param1": "value"},
         
     | 
| 42 | 
         
            -
                                },
         
     | 
| 43 | 
         
            -
                                "position": {"x": -493.5496596237119, "y": 20.90123252513356},
         
     | 
| 44 | 
         
            -
                            }
         
     | 
| 45 | 
         
            -
                        ],
         
     | 
| 46 | 
         
            -
                        "edges": [],
         
     | 
| 47 | 
         
            -
                    },
         
     | 
| 48 | 
         
            -
                }
         
     | 
| 49 | 
         
            -
                response = client.post("/api/save", json=save_request)
         
     | 
| 50 | 
         
            -
                saved_ws = response.json()
         
     | 
| 51 | 
         
            -
                assert response.status_code == 200
         
     | 
| 52 | 
         
            -
                response = client.get("/api/load?path=test")
         
     | 
| 53 | 
         
            -
                assert response.status_code == 200
         
     | 
| 54 | 
         
            -
                assert saved_ws == response.json()
         
     | 
| 55 | 
         
            -
             
     | 
| 56 | 
         
            -
             
     | 
| 57 | 
         
            -
            def test_list_dir():
         
     | 
| 58 | 
         
            -
                test_dir = str(uuid.uuid4())
         
     | 
| 59 | 
         
            -
                test_dir_full_path = DATA_PATH / test_dir
         
     | 
| 60 | 
         
            -
                test_dir_full_path.mkdir(exist_ok=True)
         
     | 
| 61 | 
         
            -
                test_file = test_dir_full_path / "test_file.txt"
         
     | 
| 62 | 
         
            -
                test_file.touch()
         
     | 
| 63 | 
         
            -
                response = client.get(f"/api/dir/list?path={str(test_dir)}")
         
     | 
| 64 | 
         
            -
                assert response.status_code == 200
         
     | 
| 65 | 
         
            -
                assert len(response.json()) == 1
         
     | 
| 66 | 
         
            -
                assert response.json()[0]["name"] == f"{test_dir}/test_file.txt"
         
     | 
| 67 | 
         
            -
                assert response.json()[0]["type"] == "workspace"
         
     | 
| 68 | 
         
            -
                test_file.unlink()
         
     | 
| 69 | 
         
            -
                test_dir_full_path.rmdir()
         
     | 
| 70 | 
         
            -
             
     | 
| 71 | 
         
            -
             
     | 
| 72 | 
         
            -
            def test_make_dir():
         
     | 
| 73 | 
         
            -
                dir_name = str(uuid.uuid4())
         
     | 
| 74 | 
         
            -
                response = client.post("/api/dir/mkdir", json={"path": dir_name})
         
     | 
| 75 | 
         
            -
                assert response.status_code == 200
         
     | 
| 76 | 
         
            -
                assert os.path.exists(DATA_PATH / dir_name)
         
     | 
| 77 | 
         
            -
                os.rmdir(DATA_PATH / dir_name)
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
    	
        lynxkite-app/web/src/apiTypes.ts
    CHANGED
    
    | 
         @@ -5,21 +5,21 @@ 
     | 
|
| 5 | 
         
             
            /* Do not modify it by hand - just update the pydantic models and then re-run the script
         
     | 
| 6 | 
         
             
            */
         
     | 
| 7 | 
         | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 8 | 
         
             
            export interface BaseConfig {
         
     | 
| 9 | 
         
             
              [k: string]: unknown;
         
     | 
| 10 | 
         
             
            }
         
     | 
| 11 | 
         
            -
            export interface  
     | 
| 12 | 
         
             
              x: number;
         
     | 
| 13 | 
         
             
              y: number;
         
     | 
| 14 | 
         
             
              [k: string]: unknown;
         
     | 
| 15 | 
         
             
            }
         
     | 
| 16 | 
         
            -
            /**
         
     | 
| 17 | 
         
            -
             * A workspace is a representation of a computational graph that consists of nodes and edges.
         
     | 
| 18 | 
         
            -
             *
         
     | 
| 19 | 
         
            -
             * Each node represents an operation or task, and the edges represent the flow of data between
         
     | 
| 20 | 
         
            -
             * the nodes. Each workspace is associated with an environment, which determines the operations
         
     | 
| 21 | 
         
            -
             * that can be performed in the workspace and the execution method for the operations.
         
     | 
| 22 | 
         
            -
             */
         
     | 
| 23 | 
         
             
            export interface Workspace {
         
     | 
| 24 | 
         
             
              env?: string;
         
     | 
| 25 | 
         
             
              nodes?: WorkspaceNode[];
         
     | 
| 
         @@ -30,7 +30,7 @@ export interface WorkspaceNode { 
     | 
|
| 30 | 
         
             
              id: string;
         
     | 
| 31 | 
         
             
              type: string;
         
     | 
| 32 | 
         
             
              data: WorkspaceNodeData;
         
     | 
| 33 | 
         
            -
              position:  
     | 
| 34 | 
         
             
              [k: string]: unknown;
         
     | 
| 35 | 
         
             
            }
         
     | 
| 36 | 
         
             
            export interface WorkspaceNodeData {
         
     | 
| 
         | 
|
| 5 | 
         
             
            /* Do not modify it by hand - just update the pydantic models and then re-run the script
         
     | 
| 6 | 
         
             
            */
         
     | 
| 7 | 
         | 
| 8 | 
         
            +
            /* eslint-disable */
         
     | 
| 9 | 
         
            +
            /**
         
     | 
| 10 | 
         
            +
             * This file was automatically generated by json-schema-to-typescript.
         
     | 
| 11 | 
         
            +
             * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
         
     | 
| 12 | 
         
            +
             * and run json-schema-to-typescript to regenerate this file.
         
     | 
| 13 | 
         
            +
             */
         
     | 
| 14 | 
         
            +
             
     | 
| 15 | 
         
             
            export interface BaseConfig {
         
     | 
| 16 | 
         
             
              [k: string]: unknown;
         
     | 
| 17 | 
         
             
            }
         
     | 
| 18 | 
         
            +
            export interface Position {
         
     | 
| 19 | 
         
             
              x: number;
         
     | 
| 20 | 
         
             
              y: number;
         
     | 
| 21 | 
         
             
              [k: string]: unknown;
         
     | 
| 22 | 
         
             
            }
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 23 | 
         
             
            export interface Workspace {
         
     | 
| 24 | 
         
             
              env?: string;
         
     | 
| 25 | 
         
             
              nodes?: WorkspaceNode[];
         
     | 
| 
         | 
|
| 30 | 
         
             
              id: string;
         
     | 
| 31 | 
         
             
              type: string;
         
     | 
| 32 | 
         
             
              data: WorkspaceNodeData;
         
     | 
| 33 | 
         
            +
              position: Position;
         
     | 
| 34 | 
         
             
              [k: string]: unknown;
         
     | 
| 35 | 
         
             
            }
         
     | 
| 36 | 
         
             
            export interface WorkspaceNodeData {
         
     | 
    	
        lynxkite-core/src/lynxkite/core/executors/one_by_one.py
    CHANGED
    
    | 
         @@ -46,19 +46,17 @@ def register(env: str, cache: bool = True): 
     | 
|
| 46 | 
         
             
                ops.EXECUTORS[env] = lambda ws: execute(ws, ops.CATALOGS[env], cache=cache)
         
     | 
| 47 | 
         | 
| 48 | 
         | 
| 49 | 
         
            -
            def get_stages(ws 
     | 
| 50 | 
         
             
                """Inputs on top/bottom are batch inputs. We decompose the graph into a DAG of components along these edges."""
         
     | 
| 51 | 
         
             
                nodes = {n.id: n for n in ws.nodes}
         
     | 
| 52 | 
         
             
                batch_inputs = {}
         
     | 
| 53 | 
         
             
                inputs = {}
         
     | 
| 54 | 
         
            -
                # For each edge in the workspacce, we record the inputs (sources)
         
     | 
| 55 | 
         
            -
                # required for each node (target).
         
     | 
| 56 | 
         
             
                for edge in ws.edges:
         
     | 
| 57 | 
         
             
                    inputs.setdefault(edge.target, []).append(edge.source)
         
     | 
| 58 | 
         
             
                    node = nodes[edge.target]
         
     | 
| 59 | 
         
             
                    op = catalog[node.data.title]
         
     | 
| 60 | 
         
             
                    i = op.inputs[edge.targetHandle]
         
     | 
| 61 | 
         
            -
                    if i. 
     | 
| 62 | 
         
             
                        batch_inputs.setdefault(edge.target, []).append(edge.source)
         
     | 
| 63 | 
         
             
                stages = []
         
     | 
| 64 | 
         
             
                for bt, bss in batch_inputs.items():
         
     | 
| 
         @@ -95,7 +93,7 @@ async def await_if_needed(obj): 
     | 
|
| 95 | 
         
             
                return obj
         
     | 
| 96 | 
         | 
| 97 | 
         | 
| 98 | 
         
            -
            async def execute(ws 
     | 
| 99 | 
         
             
                nodes = {n.id: n for n in ws.nodes}
         
     | 
| 100 | 
         
             
                contexts = {n.id: Context(node=n) for n in ws.nodes}
         
     | 
| 101 | 
         
             
                edges = {n.id: [] for n in ws.nodes}
         
     | 
| 
         @@ -110,12 +108,7 @@ async def execute(ws: workspace.Workspace, catalog: dict[str, ops.Op], cache=Non 
     | 
|
| 110 | 
         
             
                        node.data.error = f'Operation "{node.data.title}" not found.'
         
     | 
| 111 | 
         
             
                        continue
         
     | 
| 112 | 
         
             
                    # Start tasks for nodes that have no non-batch inputs.
         
     | 
| 113 | 
         
            -
                    if all(
         
     | 
| 114 | 
         
            -
                        [
         
     | 
| 115 | 
         
            -
                            i.side in [ops.Side.TOP, ops.Side.BOTTOM]
         
     | 
| 116 | 
         
            -
                            for i in op.inputs.values()
         
     | 
| 117 | 
         
            -
                        ]
         
     | 
| 118 | 
         
            -
                    ):
         
     | 
| 119 | 
         
             
                        tasks[node.id] = [NO_INPUT]
         
     | 
| 120 | 
         
             
                batch_inputs = {}
         
     | 
| 121 | 
         
             
                # Run the rest until we run out of tasks.
         
     | 
| 
         @@ -138,7 +131,7 @@ async def execute(ws: workspace.Workspace, catalog: dict[str, ops.Op], cache=Non 
     | 
|
| 138 | 
         
             
                            try:
         
     | 
| 139 | 
         
             
                                inputs = []
         
     | 
| 140 | 
         
             
                                for i in op.inputs.values():
         
     | 
| 141 | 
         
            -
                                    if i. 
     | 
| 142 | 
         
             
                                        assert (n, i.name) in batch_inputs, f"{i.name} is missing"
         
     | 
| 143 | 
         
             
                                        inputs.append(batch_inputs[(n, i.name)])
         
     | 
| 144 | 
         
             
                                    else:
         
     | 
| 
         @@ -163,16 +156,16 @@ async def execute(ws: workspace.Workspace, catalog: dict[str, ops.Op], cache=Non 
     | 
|
| 163 | 
         
             
                            results.extend(result)
         
     | 
| 164 | 
         
             
                        else:  # Finished all tasks without errors.
         
     | 
| 165 | 
         
             
                            if (
         
     | 
| 166 | 
         
            -
                                op. 
     | 
| 167 | 
         
            -
                                or op. 
     | 
| 168 | 
         
            -
                                or op. 
     | 
| 169 | 
         
             
                            ):
         
     | 
| 170 | 
         
             
                                data.display = results[0]
         
     | 
| 171 | 
         
             
                            for edge in edges[node.id]:
         
     | 
| 172 | 
         
             
                                t = nodes[edge.target]
         
     | 
| 173 | 
         
             
                                op = catalog[t.data.title]
         
     | 
| 174 | 
         
             
                                i = op.inputs[edge.targetHandle]
         
     | 
| 175 | 
         
            -
                                if i. 
     | 
| 176 | 
         
             
                                    batch_inputs.setdefault(
         
     | 
| 177 | 
         
             
                                        (edge.target, edge.targetHandle), []
         
     | 
| 178 | 
         
             
                                    ).extend(results)
         
     | 
| 
         | 
|
| 46 | 
         
             
                ops.EXECUTORS[env] = lambda ws: execute(ws, ops.CATALOGS[env], cache=cache)
         
     | 
| 47 | 
         | 
| 48 | 
         | 
| 49 | 
         
            +
            def get_stages(ws, catalog):
         
     | 
| 50 | 
         
             
                """Inputs on top/bottom are batch inputs. We decompose the graph into a DAG of components along these edges."""
         
     | 
| 51 | 
         
             
                nodes = {n.id: n for n in ws.nodes}
         
     | 
| 52 | 
         
             
                batch_inputs = {}
         
     | 
| 53 | 
         
             
                inputs = {}
         
     | 
| 
         | 
|
| 
         | 
|
| 54 | 
         
             
                for edge in ws.edges:
         
     | 
| 55 | 
         
             
                    inputs.setdefault(edge.target, []).append(edge.source)
         
     | 
| 56 | 
         
             
                    node = nodes[edge.target]
         
     | 
| 57 | 
         
             
                    op = catalog[node.data.title]
         
     | 
| 58 | 
         
             
                    i = op.inputs[edge.targetHandle]
         
     | 
| 59 | 
         
            +
                    if i.position in "top or bottom":
         
     | 
| 60 | 
         
             
                        batch_inputs.setdefault(edge.target, []).append(edge.source)
         
     | 
| 61 | 
         
             
                stages = []
         
     | 
| 62 | 
         
             
                for bt, bss in batch_inputs.items():
         
     | 
| 
         | 
|
| 93 | 
         
             
                return obj
         
     | 
| 94 | 
         | 
| 95 | 
         | 
| 96 | 
         
            +
            async def execute(ws, catalog, cache=None):
         
     | 
| 97 | 
         
             
                nodes = {n.id: n for n in ws.nodes}
         
     | 
| 98 | 
         
             
                contexts = {n.id: Context(node=n) for n in ws.nodes}
         
     | 
| 99 | 
         
             
                edges = {n.id: [] for n in ws.nodes}
         
     | 
| 
         | 
|
| 108 | 
         
             
                        node.data.error = f'Operation "{node.data.title}" not found.'
         
     | 
| 109 | 
         
             
                        continue
         
     | 
| 110 | 
         
             
                    # Start tasks for nodes that have no non-batch inputs.
         
     | 
| 111 | 
         
            +
                    if all([i.position in "top or bottom" for i in op.inputs.values()]):
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 112 | 
         
             
                        tasks[node.id] = [NO_INPUT]
         
     | 
| 113 | 
         
             
                batch_inputs = {}
         
     | 
| 114 | 
         
             
                # Run the rest until we run out of tasks.
         
     | 
| 
         | 
|
| 131 | 
         
             
                            try:
         
     | 
| 132 | 
         
             
                                inputs = []
         
     | 
| 133 | 
         
             
                                for i in op.inputs.values():
         
     | 
| 134 | 
         
            +
                                    if i.position in "top or bottom":
         
     | 
| 135 | 
         
             
                                        assert (n, i.name) in batch_inputs, f"{i.name} is missing"
         
     | 
| 136 | 
         
             
                                        inputs.append(batch_inputs[(n, i.name)])
         
     | 
| 137 | 
         
             
                                    else:
         
     | 
| 
         | 
|
| 156 | 
         
             
                            results.extend(result)
         
     | 
| 157 | 
         
             
                        else:  # Finished all tasks without errors.
         
     | 
| 158 | 
         
             
                            if (
         
     | 
| 159 | 
         
            +
                                op.type == "visualization"
         
     | 
| 160 | 
         
            +
                                or op.type == "table_view"
         
     | 
| 161 | 
         
            +
                                or op.type == "image"
         
     | 
| 162 | 
         
             
                            ):
         
     | 
| 163 | 
         
             
                                data.display = results[0]
         
     | 
| 164 | 
         
             
                            for edge in edges[node.id]:
         
     | 
| 165 | 
         
             
                                t = nodes[edge.target]
         
     | 
| 166 | 
         
             
                                op = catalog[t.data.title]
         
     | 
| 167 | 
         
             
                                i = op.inputs[edge.targetHandle]
         
     | 
| 168 | 
         
            +
                                if i.position in "top or bottom":
         
     | 
| 169 | 
         
             
                                    batch_inputs.setdefault(
         
     | 
| 170 | 
         
             
                                        (edge.target, edge.targetHandle), []
         
     | 
| 171 | 
         
             
                                    ).extend(results)
         
     | 
    	
        lynxkite-core/src/lynxkite/core/ops.py
    CHANGED
    
    | 
         @@ -61,23 +61,16 @@ class Parameter(BaseConfig): 
     | 
|
| 61 | 
         
             
                    return Parameter(name=name, default=default, type=type)
         
     | 
| 62 | 
         | 
| 63 | 
         | 
| 64 | 
         
            -
            class Side(enum.StrEnum):
         
     | 
| 65 | 
         
            -
                LEFT = "left"
         
     | 
| 66 | 
         
            -
                RIGHT = "right"
         
     | 
| 67 | 
         
            -
                TOP = "top"
         
     | 
| 68 | 
         
            -
                BOTTOM = "bottom"
         
     | 
| 69 | 
         
            -
             
     | 
| 70 | 
         
            -
             
     | 
| 71 | 
         
             
            class Input(BaseConfig):
         
     | 
| 72 | 
         
             
                name: str
         
     | 
| 73 | 
         
             
                type: Type
         
     | 
| 74 | 
         
            -
                 
     | 
| 75 | 
         | 
| 76 | 
         | 
| 77 | 
         
             
            class Output(BaseConfig):
         
     | 
| 78 | 
         
             
                name: str
         
     | 
| 79 | 
         
             
                type: Type
         
     | 
| 80 | 
         
            -
                 
     | 
| 81 | 
         | 
| 82 | 
         | 
| 83 | 
         
             
            MULTI_INPUT = Input(name="multi", type="*")
         
     | 
| 
         @@ -91,22 +84,13 @@ def basic_outputs(*names): 
     | 
|
| 91 | 
         
             
                return {name: Output(name=name, type=None) for name in names}
         
     | 
| 92 | 
         | 
| 93 | 
         | 
| 94 | 
         
            -
            class ViewType(enum.StrEnum):
         
     | 
| 95 | 
         
            -
                """Represents the visualization options for an operation."""
         
     | 
| 96 | 
         
            -
             
     | 
| 97 | 
         
            -
                BASIC = "basic"
         
     | 
| 98 | 
         
            -
                VISUALIZATION = "visualization"
         
     | 
| 99 | 
         
            -
                IMAGE = "image"
         
     | 
| 100 | 
         
            -
                TABLE_VIEW = "table_view"
         
     | 
| 101 | 
         
            -
             
     | 
| 102 | 
         
            -
             
     | 
| 103 | 
         
             
            class Op(BaseConfig):
         
     | 
| 104 | 
         
             
                func: typing.Callable = pydantic.Field(exclude=True)
         
     | 
| 105 | 
         
             
                name: str
         
     | 
| 106 | 
         
             
                params: dict[str, Parameter]
         
     | 
| 107 | 
         
             
                inputs: dict[str, Input]
         
     | 
| 108 | 
         
             
                outputs: dict[str, Output]
         
     | 
| 109 | 
         
            -
                 
     | 
| 110 | 
         | 
| 111 | 
         
             
                def __call__(self, *inputs, **params):
         
     | 
| 112 | 
         
             
                    # Convert parameters.
         
     | 
| 
         @@ -149,7 +133,7 @@ def op(env: str, name: str, *, view="basic", outputs=None): 
     | 
|
| 149 | 
         
             
                        params=params,
         
     | 
| 150 | 
         
             
                        inputs=inputs,
         
     | 
| 151 | 
         
             
                        outputs=_outputs,
         
     | 
| 152 | 
         
            -
                         
     | 
| 153 | 
         
             
                    )
         
     | 
| 154 | 
         
             
                    CATALOGS.setdefault(env, {})
         
     | 
| 155 | 
         
             
                    CATALOGS[env][name] = op
         
     | 
| 
         @@ -159,25 +143,25 @@ def op(env: str, name: str, *, view="basic", outputs=None): 
     | 
|
| 159 | 
         
             
                return decorator
         
     | 
| 160 | 
         | 
| 161 | 
         | 
| 162 | 
         
            -
            def  
     | 
| 163 | 
         
            -
                """Decorator for specifying unusual  
     | 
| 164 | 
         | 
| 165 | 
         
             
                def decorator(func):
         
     | 
| 166 | 
         
             
                    op = func.__op__
         
     | 
| 167 | 
         
             
                    for k, v in kwargs.items():
         
     | 
| 168 | 
         
            -
                        op.inputs[k]. 
     | 
| 169 | 
         
             
                    return func
         
     | 
| 170 | 
         | 
| 171 | 
         
             
                return decorator
         
     | 
| 172 | 
         | 
| 173 | 
         | 
| 174 | 
         
            -
            def  
     | 
| 175 | 
         
            -
                """Decorator for specifying unusual  
     | 
| 176 | 
         | 
| 177 | 
         
             
                def decorator(func):
         
     | 
| 178 | 
         
             
                    op = func.__op__
         
     | 
| 179 | 
         
             
                    for k, v in kwargs.items():
         
     | 
| 180 | 
         
            -
                        op.outputs[k]. 
     | 
| 181 | 
         
             
                    return func
         
     | 
| 182 | 
         | 
| 183 | 
         
             
                return decorator
         
     | 
| 
         @@ -189,13 +173,7 @@ def no_op(*args, **kwargs): 
     | 
|
| 189 | 
         
             
                return None
         
     | 
| 190 | 
         | 
| 191 | 
         | 
| 192 | 
         
            -
            def register_passive_op(
         
     | 
| 193 | 
         
            -
                env: str,
         
     | 
| 194 | 
         
            -
                name: str,
         
     | 
| 195 | 
         
            -
                inputs: list[Input] = [],
         
     | 
| 196 | 
         
            -
                outputs: list[Output] = ["output"],
         
     | 
| 197 | 
         
            -
                params: list[Parameter] = [],
         
     | 
| 198 | 
         
            -
            ):
         
     | 
| 199 | 
         
             
                """A passive operation has no associated code."""
         
     | 
| 200 | 
         
             
                op = Op(
         
     | 
| 201 | 
         
             
                    func=no_op,
         
     | 
| 
         @@ -231,3 +209,16 @@ def op_registration(env: str): 
     | 
|
| 231 | 
         | 
| 232 | 
         
             
            def passive_op_registration(env: str):
         
     | 
| 233 | 
         
             
                return functools.partial(register_passive_op, env)
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 61 | 
         
             
                    return Parameter(name=name, default=default, type=type)
         
     | 
| 62 | 
         | 
| 63 | 
         | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 64 | 
         
             
            class Input(BaseConfig):
         
     | 
| 65 | 
         
             
                name: str
         
     | 
| 66 | 
         
             
                type: Type
         
     | 
| 67 | 
         
            +
                position: str = "left"
         
     | 
| 68 | 
         | 
| 69 | 
         | 
| 70 | 
         
             
            class Output(BaseConfig):
         
     | 
| 71 | 
         
             
                name: str
         
     | 
| 72 | 
         
             
                type: Type
         
     | 
| 73 | 
         
            +
                position: str = "right"
         
     | 
| 74 | 
         | 
| 75 | 
         | 
| 76 | 
         
             
            MULTI_INPUT = Input(name="multi", type="*")
         
     | 
| 
         | 
|
| 84 | 
         
             
                return {name: Output(name=name, type=None) for name in names}
         
     | 
| 85 | 
         | 
| 86 | 
         | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 87 | 
         
             
            class Op(BaseConfig):
         
     | 
| 88 | 
         
             
                func: typing.Callable = pydantic.Field(exclude=True)
         
     | 
| 89 | 
         
             
                name: str
         
     | 
| 90 | 
         
             
                params: dict[str, Parameter]
         
     | 
| 91 | 
         
             
                inputs: dict[str, Input]
         
     | 
| 92 | 
         
             
                outputs: dict[str, Output]
         
     | 
| 93 | 
         
            +
                type: str = "basic"  # The UI to use for this operation.
         
     | 
| 94 | 
         | 
| 95 | 
         
             
                def __call__(self, *inputs, **params):
         
     | 
| 96 | 
         
             
                    # Convert parameters.
         
     | 
| 
         | 
|
| 133 | 
         
             
                        params=params,
         
     | 
| 134 | 
         
             
                        inputs=inputs,
         
     | 
| 135 | 
         
             
                        outputs=_outputs,
         
     | 
| 136 | 
         
            +
                        type=view,
         
     | 
| 137 | 
         
             
                    )
         
     | 
| 138 | 
         
             
                    CATALOGS.setdefault(env, {})
         
     | 
| 139 | 
         
             
                    CATALOGS[env][name] = op
         
     | 
| 
         | 
|
| 143 | 
         
             
                return decorator
         
     | 
| 144 | 
         | 
| 145 | 
         | 
| 146 | 
         
            +
            def input_position(**kwargs):
         
     | 
| 147 | 
         
            +
                """Decorator for specifying unusual positions for the inputs."""
         
     | 
| 148 | 
         | 
| 149 | 
         
             
                def decorator(func):
         
     | 
| 150 | 
         
             
                    op = func.__op__
         
     | 
| 151 | 
         
             
                    for k, v in kwargs.items():
         
     | 
| 152 | 
         
            +
                        op.inputs[k].position = v
         
     | 
| 153 | 
         
             
                    return func
         
     | 
| 154 | 
         | 
| 155 | 
         
             
                return decorator
         
     | 
| 156 | 
         | 
| 157 | 
         | 
| 158 | 
         
            +
            def output_position(**kwargs):
         
     | 
| 159 | 
         
            +
                """Decorator for specifying unusual positions for the outputs."""
         
     | 
| 160 | 
         | 
| 161 | 
         
             
                def decorator(func):
         
     | 
| 162 | 
         
             
                    op = func.__op__
         
     | 
| 163 | 
         
             
                    for k, v in kwargs.items():
         
     | 
| 164 | 
         
            +
                        op.outputs[k].position = v
         
     | 
| 165 | 
         
             
                    return func
         
     | 
| 166 | 
         | 
| 167 | 
         
             
                return decorator
         
     | 
| 
         | 
|
| 173 | 
         
             
                return None
         
     | 
| 174 | 
         | 
| 175 | 
         | 
| 176 | 
         
            +
            def register_passive_op(env: str, name: str, inputs=[], outputs=["output"], params=[]):
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 177 | 
         
             
                """A passive operation has no associated code."""
         
     | 
| 178 | 
         
             
                op = Op(
         
     | 
| 179 | 
         
             
                    func=no_op,
         
     | 
| 
         | 
|
| 209 | 
         | 
| 210 | 
         
             
            def passive_op_registration(env: str):
         
     | 
| 211 | 
         
             
                return functools.partial(register_passive_op, env)
         
     | 
| 212 | 
         
            +
             
     | 
| 213 | 
         
            +
             
     | 
| 214 | 
         
            +
            def register_area(env, name, params=[]):
         
     | 
| 215 | 
         
            +
                """A node that represents an area. It can contain other nodes, but does not restrict movement in any way."""
         
     | 
| 216 | 
         
            +
                op = Op(
         
     | 
| 217 | 
         
            +
                    func=no_op,
         
     | 
| 218 | 
         
            +
                    name=name,
         
     | 
| 219 | 
         
            +
                    params={p.name: p for p in params},
         
     | 
| 220 | 
         
            +
                    inputs={},
         
     | 
| 221 | 
         
            +
                    outputs={},
         
     | 
| 222 | 
         
            +
                    type="area",
         
     | 
| 223 | 
         
            +
                )
         
     | 
| 224 | 
         
            +
                CATALOGS[env][name] = op
         
     | 
    	
        lynxkite-core/src/lynxkite/core/workspace.py
    CHANGED
    
    | 
         @@ -14,8 +14,7 @@ class BaseConfig(pydantic.BaseModel): 
     | 
|
| 14 | 
         
             
                )
         
     | 
| 15 | 
         | 
| 16 | 
         | 
| 17 | 
         
            -
             
     | 
| 18 | 
         
            -
            class NodePosition(BaseConfig):
         
     | 
| 19 | 
         
             
                x: float
         
     | 
| 20 | 
         
             
                y: float
         
     | 
| 21 | 
         | 
| 
         @@ -30,12 +29,10 @@ class WorkspaceNodeData(BaseConfig): 
     | 
|
| 30 | 
         | 
| 31 | 
         | 
| 32 | 
         
             
            class WorkspaceNode(BaseConfig):
         
     | 
| 33 | 
         
            -
                # The naming of these attributes matches the ones for the NodeBase type in React flow
         
     | 
| 34 | 
         
            -
                # modyfing them will break the frontend.
         
     | 
| 35 | 
         
             
                id: str
         
     | 
| 36 | 
         
             
                type: str
         
     | 
| 37 | 
         
             
                data: WorkspaceNodeData
         
     | 
| 38 | 
         
            -
                position:  
     | 
| 39 | 
         | 
| 40 | 
         | 
| 41 | 
         
             
            class WorkspaceEdge(BaseConfig):
         
     | 
| 
         @@ -47,13 +44,6 @@ class WorkspaceEdge(BaseConfig): 
     | 
|
| 47 | 
         | 
| 48 | 
         | 
| 49 | 
         
             
            class Workspace(BaseConfig):
         
     | 
| 50 | 
         
            -
                """A workspace is a representation of a computational graph that consists of nodes and edges.
         
     | 
| 51 | 
         
            -
             
     | 
| 52 | 
         
            -
                Each node represents an operation or task, and the edges represent the flow of data between
         
     | 
| 53 | 
         
            -
                the nodes. Each workspace is associated with an environment, which determines the operations
         
     | 
| 54 | 
         
            -
                that can be performed in the workspace and the execution method for the operations.
         
     | 
| 55 | 
         
            -
                """
         
     | 
| 56 | 
         
            -
             
     | 
| 57 | 
         
             
                env: str = ""
         
     | 
| 58 | 
         
             
                nodes: list[WorkspaceNode] = dataclasses.field(default_factory=list)
         
     | 
| 59 | 
         
             
                edges: list[WorkspaceEdge] = dataclasses.field(default_factory=list)
         
     | 
| 
         @@ -65,7 +55,6 @@ async def execute(ws: Workspace): 
     | 
|
| 65 | 
         | 
| 66 | 
         | 
| 67 | 
         
             
            def save(ws: Workspace, path: str):
         
     | 
| 68 | 
         
            -
                """Persist a workspace to a local file in JSON format."""
         
     | 
| 69 | 
         
             
                j = ws.model_dump_json(indent=2)
         
     | 
| 70 | 
         
             
                dirname, basename = os.path.split(path)
         
     | 
| 71 | 
         
             
                # Create temp file in the same directory to make sure it's on the same filesystem.
         
     | 
| 
         @@ -77,17 +66,7 @@ def save(ws: Workspace, path: str): 
     | 
|
| 77 | 
         
             
                os.replace(temp_name, path)
         
     | 
| 78 | 
         | 
| 79 | 
         | 
| 80 | 
         
            -
            def load(path: str) 
     | 
| 81 | 
         
            -
                """Load a workspace from a file.
         
     | 
| 82 | 
         
            -
             
     | 
| 83 | 
         
            -
                After loading the workspace, the metadata of the workspace is updated.
         
     | 
| 84 | 
         
            -
             
     | 
| 85 | 
         
            -
                Args:
         
     | 
| 86 | 
         
            -
                    path (str): The path to the file to load the workspace from.
         
     | 
| 87 | 
         
            -
             
     | 
| 88 | 
         
            -
                Returns:
         
     | 
| 89 | 
         
            -
                    Workspace: The loaded workspace object, with updated metadata.
         
     | 
| 90 | 
         
            -
                """
         
     | 
| 91 | 
         
             
                with open(path) as f:
         
     | 
| 92 | 
         
             
                    j = f.read()
         
     | 
| 93 | 
         
             
                ws = Workspace.model_validate_json(j)
         
     | 
| 
         @@ -96,32 +75,19 @@ def load(path: str) -> Workspace: 
     | 
|
| 96 | 
         
             
                return ws
         
     | 
| 97 | 
         | 
| 98 | 
         | 
| 99 | 
         
            -
            def _update_metadata(ws 
     | 
| 100 | 
         
            -
                 
     | 
| 101 | 
         
            -
             
     | 
| 102 | 
         
            -
                The metadata is the information about the operations that the nodes in the workspace represent,
         
     | 
| 103 | 
         
            -
                like the parameters and their possible values.
         
     | 
| 104 | 
         
            -
                This information comes from the catalog of operations for the environment of the workspace.
         
     | 
| 105 | 
         
            -
             
     | 
| 106 | 
         
            -
                Args:
         
     | 
| 107 | 
         
            -
                    ws: The workspace object to update.
         
     | 
| 108 | 
         
            -
             
     | 
| 109 | 
         
            -
                Returns:
         
     | 
| 110 | 
         
            -
                    Workspace: The updated workspace object.
         
     | 
| 111 | 
         
            -
                """
         
     | 
| 112 | 
         
            -
                catalog: dict[str, ops.Op] = ops.CATALOGS.get(ws.env, {})
         
     | 
| 113 | 
         
             
                nodes = {node.id: node for node in ws.nodes}
         
     | 
| 114 | 
         
             
                done = set()
         
     | 
| 115 | 
         
             
                while len(done) < len(nodes):
         
     | 
| 116 | 
         
             
                    for node in ws.nodes:
         
     | 
| 117 | 
         
             
                        if node.id in done:
         
     | 
| 118 | 
         
            -
                            # TODO: Can nodes with the same ID reference different operations?
         
     | 
| 119 | 
         
             
                            continue
         
     | 
| 120 | 
         
             
                        data = node.data
         
     | 
| 121 | 
         
             
                        op = catalog.get(data.title)
         
     | 
| 122 | 
         
             
                        if op:
         
     | 
| 123 | 
         
             
                            data.meta = op
         
     | 
| 124 | 
         
            -
                            node.type = op. 
     | 
| 125 | 
         
             
                            if data.error == "Unknown operation.":
         
     | 
| 126 | 
         
             
                                data.error = None
         
     | 
| 127 | 
         
             
                        else:
         
     | 
| 
         | 
|
| 14 | 
         
             
                )
         
     | 
| 15 | 
         | 
| 16 | 
         | 
| 17 | 
         
            +
            class Position(BaseConfig):
         
     | 
| 
         | 
|
| 18 | 
         
             
                x: float
         
     | 
| 19 | 
         
             
                y: float
         
     | 
| 20 | 
         | 
| 
         | 
|
| 29 | 
         | 
| 30 | 
         | 
| 31 | 
         
             
            class WorkspaceNode(BaseConfig):
         
     | 
| 
         | 
|
| 
         | 
|
| 32 | 
         
             
                id: str
         
     | 
| 33 | 
         
             
                type: str
         
     | 
| 34 | 
         
             
                data: WorkspaceNodeData
         
     | 
| 35 | 
         
            +
                position: Position
         
     | 
| 36 | 
         | 
| 37 | 
         | 
| 38 | 
         
             
            class WorkspaceEdge(BaseConfig):
         
     | 
| 
         | 
|
| 44 | 
         | 
| 45 | 
         | 
| 46 | 
         
             
            class Workspace(BaseConfig):
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 47 | 
         
             
                env: str = ""
         
     | 
| 48 | 
         
             
                nodes: list[WorkspaceNode] = dataclasses.field(default_factory=list)
         
     | 
| 49 | 
         
             
                edges: list[WorkspaceEdge] = dataclasses.field(default_factory=list)
         
     | 
| 
         | 
|
| 55 | 
         | 
| 56 | 
         | 
| 57 | 
         
             
            def save(ws: Workspace, path: str):
         
     | 
| 
         | 
|
| 58 | 
         
             
                j = ws.model_dump_json(indent=2)
         
     | 
| 59 | 
         
             
                dirname, basename = os.path.split(path)
         
     | 
| 60 | 
         
             
                # Create temp file in the same directory to make sure it's on the same filesystem.
         
     | 
| 
         | 
|
| 66 | 
         
             
                os.replace(temp_name, path)
         
     | 
| 67 | 
         | 
| 68 | 
         | 
| 69 | 
         
            +
            def load(path: str):
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 70 | 
         
             
                with open(path) as f:
         
     | 
| 71 | 
         
             
                    j = f.read()
         
     | 
| 72 | 
         
             
                ws = Workspace.model_validate_json(j)
         
     | 
| 
         | 
|
| 75 | 
         
             
                return ws
         
     | 
| 76 | 
         | 
| 77 | 
         | 
| 78 | 
         
            +
            def _update_metadata(ws):
         
     | 
| 79 | 
         
            +
                catalog = ops.CATALOGS.get(ws.env, {})
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 80 | 
         
             
                nodes = {node.id: node for node in ws.nodes}
         
     | 
| 81 | 
         
             
                done = set()
         
     | 
| 82 | 
         
             
                while len(done) < len(nodes):
         
     | 
| 83 | 
         
             
                    for node in ws.nodes:
         
     | 
| 84 | 
         
             
                        if node.id in done:
         
     | 
| 
         | 
|
| 85 | 
         
             
                            continue
         
     | 
| 86 | 
         
             
                        data = node.data
         
     | 
| 87 | 
         
             
                        op = catalog.get(data.title)
         
     | 
| 88 | 
         
             
                        if op:
         
     | 
| 89 | 
         
             
                            data.meta = op
         
     | 
| 90 | 
         
            +
                            node.type = op.type
         
     | 
| 91 | 
         
             
                            if data.error == "Unknown operation.":
         
     | 
| 92 | 
         
             
                                data.error = None
         
     | 
| 93 | 
         
             
                        else:
         
     | 
    	
        lynxkite-core/tests/test_ops.py
    DELETED
    
    | 
         @@ -1,89 +0,0 @@ 
     | 
|
| 1 | 
         
            -
            import inspect
         
     | 
| 2 | 
         
            -
            from lynxkite.core import ops
         
     | 
| 3 | 
         
            -
            import enum
         
     | 
| 4 | 
         
            -
             
     | 
| 5 | 
         
            -
             
     | 
| 6 | 
         
            -
            def test_op_decorator_no_params_no_types_default_sides():
         
     | 
| 7 | 
         
            -
                @ops.op(env="test", name="add", view=ops.ViewType.BASIC, outputs=["result"])
         
     | 
| 8 | 
         
            -
                def add(a, b):
         
     | 
| 9 | 
         
            -
                    return a + b
         
     | 
| 10 | 
         
            -
             
     | 
| 11 | 
         
            -
                assert add.__op__.name == "add"
         
     | 
| 12 | 
         
            -
                assert add.__op__.params == {}
         
     | 
| 13 | 
         
            -
                assert add.__op__.inputs == {
         
     | 
| 14 | 
         
            -
                    "a": ops.Input(name="a", type=inspect._empty, side=ops.Side.LEFT),
         
     | 
| 15 | 
         
            -
                    "b": ops.Input(name="b", type=inspect._empty, side=ops.Side.LEFT),
         
     | 
| 16 | 
         
            -
                }
         
     | 
| 17 | 
         
            -
                assert add.__op__.outputs == {
         
     | 
| 18 | 
         
            -
                    "result": ops.Output(name="result", type=None, side=ops.Side.RIGHT)
         
     | 
| 19 | 
         
            -
                }
         
     | 
| 20 | 
         
            -
                assert add.__op__.view_type == ops.ViewType.BASIC
         
     | 
| 21 | 
         
            -
                assert ops.CATALOGS["test"]["add"] == add.__op__
         
     | 
| 22 | 
         
            -
             
     | 
| 23 | 
         
            -
             
     | 
| 24 | 
         
            -
            def test_op_decorator_custom_sides():
         
     | 
| 25 | 
         
            -
                @ops.input_side(a=ops.Side.RIGHT, b=ops.Side.TOP)
         
     | 
| 26 | 
         
            -
                @ops.output_side(result=ops.Side.BOTTOM)
         
     | 
| 27 | 
         
            -
                @ops.op(env="test", name="add", view=ops.ViewType.BASIC, outputs=["result"])
         
     | 
| 28 | 
         
            -
                def add(a, b):
         
     | 
| 29 | 
         
            -
                    return a + b
         
     | 
| 30 | 
         
            -
             
     | 
| 31 | 
         
            -
                assert add.__op__.name == "add"
         
     | 
| 32 | 
         
            -
                assert add.__op__.params == {}
         
     | 
| 33 | 
         
            -
                assert add.__op__.inputs == {
         
     | 
| 34 | 
         
            -
                    "a": ops.Input(name="a", type=inspect._empty, side=ops.Side.RIGHT),
         
     | 
| 35 | 
         
            -
                    "b": ops.Input(name="b", type=inspect._empty, side=ops.Side.TOP),
         
     | 
| 36 | 
         
            -
                }
         
     | 
| 37 | 
         
            -
                assert add.__op__.outputs == {
         
     | 
| 38 | 
         
            -
                    "result": ops.Output(name="result", type=None, side=ops.Side.BOTTOM)
         
     | 
| 39 | 
         
            -
                }
         
     | 
| 40 | 
         
            -
                assert add.__op__.view_type == ops.ViewType.BASIC
         
     | 
| 41 | 
         
            -
                assert ops.CATALOGS["test"]["add"] == add.__op__
         
     | 
| 42 | 
         
            -
             
     | 
| 43 | 
         
            -
             
     | 
| 44 | 
         
            -
            def test_op_decorator_with_params_and_types_():
         
     | 
| 45 | 
         
            -
                @ops.op(env="test", name="multiply", view=ops.ViewType.BASIC, outputs=["result"])
         
     | 
| 46 | 
         
            -
                def multiply(a: int, b: float = 2.0, *, param: str = "param"):
         
     | 
| 47 | 
         
            -
                    return a * b
         
     | 
| 48 | 
         
            -
             
     | 
| 49 | 
         
            -
                assert multiply.__op__.name == "multiply"
         
     | 
| 50 | 
         
            -
                assert multiply.__op__.params == {
         
     | 
| 51 | 
         
            -
                    "param": ops.Parameter(name="param", default="param", type=str)
         
     | 
| 52 | 
         
            -
                }
         
     | 
| 53 | 
         
            -
                assert multiply.__op__.inputs == {
         
     | 
| 54 | 
         
            -
                    "a": ops.Input(name="a", type=int, side=ops.Side.LEFT),
         
     | 
| 55 | 
         
            -
                    "b": ops.Input(name="b", type=float, side=ops.Side.LEFT),
         
     | 
| 56 | 
         
            -
                }
         
     | 
| 57 | 
         
            -
                assert multiply.__op__.outputs == {
         
     | 
| 58 | 
         
            -
                    "result": ops.Output(name="result", type=None, side=ops.Side.RIGHT)
         
     | 
| 59 | 
         
            -
                }
         
     | 
| 60 | 
         
            -
                assert multiply.__op__.view_type == ops.ViewType.BASIC
         
     | 
| 61 | 
         
            -
                assert ops.CATALOGS["test"]["multiply"] == multiply.__op__
         
     | 
| 62 | 
         
            -
             
     | 
| 63 | 
         
            -
             
     | 
| 64 | 
         
            -
            def test_op_decorator_with_complex_types():
         
     | 
| 65 | 
         
            -
                class Color(enum.Enum):
         
     | 
| 66 | 
         
            -
                    RED = 1
         
     | 
| 67 | 
         
            -
                    GREEN = 2
         
     | 
| 68 | 
         
            -
                    BLUE = 3
         
     | 
| 69 | 
         
            -
             
     | 
| 70 | 
         
            -
                @ops.op(env="test", name="color_op", view=ops.ViewType.BASIC, outputs=["result"])
         
     | 
| 71 | 
         
            -
                def complex_op(color: Color, color_list: list[Color], color_dict: dict[str, Color]):
         
     | 
| 72 | 
         
            -
                    return color.name
         
     | 
| 73 | 
         
            -
             
     | 
| 74 | 
         
            -
                assert complex_op.__op__.name == "color_op"
         
     | 
| 75 | 
         
            -
                assert complex_op.__op__.params == {}
         
     | 
| 76 | 
         
            -
                assert complex_op.__op__.inputs == {
         
     | 
| 77 | 
         
            -
                    "color": ops.Input(name="color", type=Color, side=ops.Side.LEFT),
         
     | 
| 78 | 
         
            -
                    "color_list": ops.Input(
         
     | 
| 79 | 
         
            -
                        name="color_list", type=list[Color], side=ops.Side.LEFT
         
     | 
| 80 | 
         
            -
                    ),
         
     | 
| 81 | 
         
            -
                    "color_dict": ops.Input(
         
     | 
| 82 | 
         
            -
                        name="color_dict", type=dict[str, Color], side=ops.Side.LEFT
         
     | 
| 83 | 
         
            -
                    ),
         
     | 
| 84 | 
         
            -
                }
         
     | 
| 85 | 
         
            -
                assert complex_op.__op__.view_type == ops.ViewType.BASIC
         
     | 
| 86 | 
         
            -
                assert complex_op.__op__.outputs == {
         
     | 
| 87 | 
         
            -
                    "result": ops.Output(name="result", type=None, side=ops.Side.RIGHT)
         
     | 
| 88 | 
         
            -
                }
         
     | 
| 89 | 
         
            -
                assert ops.CATALOGS["test"]["color_op"] == complex_op.__op__
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
    	
        lynxkite-core/tests/test_workspace.py
    DELETED
    
    | 
         @@ -1,115 +0,0 @@ 
     | 
|
| 1 | 
         
            -
            import os
         
     | 
| 2 | 
         
            -
            import pytest
         
     | 
| 3 | 
         
            -
            import tempfile
         
     | 
| 4 | 
         
            -
            from lynxkite.core import workspace
         
     | 
| 5 | 
         
            -
            from lynxkite.core import ops
         
     | 
| 6 | 
         
            -
             
     | 
| 7 | 
         
            -
             
     | 
| 8 | 
         
            -
            def test_save_load():
         
     | 
| 9 | 
         
            -
                ws = workspace.Workspace(env="test")
         
     | 
| 10 | 
         
            -
                ws.nodes.append(
         
     | 
| 11 | 
         
            -
                    workspace.WorkspaceNode(
         
     | 
| 12 | 
         
            -
                        id="1",
         
     | 
| 13 | 
         
            -
                        type="node_type",
         
     | 
| 14 | 
         
            -
                        data=workspace.WorkspaceNodeData(title="Node 1", params={}),
         
     | 
| 15 | 
         
            -
                        position=workspace.NodePosition(x=0, y=0),
         
     | 
| 16 | 
         
            -
                    )
         
     | 
| 17 | 
         
            -
                )
         
     | 
| 18 | 
         
            -
                ws.nodes.append(
         
     | 
| 19 | 
         
            -
                    workspace.WorkspaceNode(
         
     | 
| 20 | 
         
            -
                        id="2",
         
     | 
| 21 | 
         
            -
                        type="node_type",
         
     | 
| 22 | 
         
            -
                        data=workspace.WorkspaceNodeData(title="Node 2", params={}),
         
     | 
| 23 | 
         
            -
                        position=workspace.NodePosition(x=0, y=0),
         
     | 
| 24 | 
         
            -
                    )
         
     | 
| 25 | 
         
            -
                )
         
     | 
| 26 | 
         
            -
                ws.edges.append(
         
     | 
| 27 | 
         
            -
                    workspace.WorkspaceEdge(
         
     | 
| 28 | 
         
            -
                        id="edge1",
         
     | 
| 29 | 
         
            -
                        source="1",
         
     | 
| 30 | 
         
            -
                        target="2",
         
     | 
| 31 | 
         
            -
                        sourceHandle="",
         
     | 
| 32 | 
         
            -
                        targetHandle="",
         
     | 
| 33 | 
         
            -
                    )
         
     | 
| 34 | 
         
            -
                )
         
     | 
| 35 | 
         
            -
                path = os.path.join(tempfile.gettempdir(), "test_workspace.json")
         
     | 
| 36 | 
         
            -
             
     | 
| 37 | 
         
            -
                try:
         
     | 
| 38 | 
         
            -
                    workspace.save(ws, path)
         
     | 
| 39 | 
         
            -
                    assert os.path.exists(path)
         
     | 
| 40 | 
         
            -
                    loaded_ws = workspace.load(path)
         
     | 
| 41 | 
         
            -
                    assert loaded_ws.env == ws.env
         
     | 
| 42 | 
         
            -
                    assert len(loaded_ws.nodes) == len(ws.nodes)
         
     | 
| 43 | 
         
            -
                    assert len(loaded_ws.edges) == len(ws.edges)
         
     | 
| 44 | 
         
            -
                    sorted_ws_nodes = sorted(ws.nodes, key=lambda x: x.id)
         
     | 
| 45 | 
         
            -
                    sorted_loaded_ws_nodes = sorted(loaded_ws.nodes, key=lambda x: x.id)
         
     | 
| 46 | 
         
            -
                    # We do manual assertion on each attribute because metadata is added at
         
     | 
| 47 | 
         
            -
                    # loading time, which makes the objects different.
         
     | 
| 48 | 
         
            -
                    for node, loaded_node in zip(sorted_ws_nodes, sorted_loaded_ws_nodes):
         
     | 
| 49 | 
         
            -
                        assert node.id == loaded_node.id
         
     | 
| 50 | 
         
            -
                        assert node.type == loaded_node.type
         
     | 
| 51 | 
         
            -
                        assert node.data.title == loaded_node.data.title
         
     | 
| 52 | 
         
            -
                        assert node.data.params == loaded_node.data.params
         
     | 
| 53 | 
         
            -
                        assert node.position.x == loaded_node.position.x
         
     | 
| 54 | 
         
            -
                        assert node.position.y == loaded_node.position.y
         
     | 
| 55 | 
         
            -
                    sorted_ws_edges = sorted(ws.edges, key=lambda x: x.id)
         
     | 
| 56 | 
         
            -
                    sorted_loaded_ws_edges = sorted(loaded_ws.edges, key=lambda x: x.id)
         
     | 
| 57 | 
         
            -
                    for edge, loaded_edge in zip(sorted_ws_edges, sorted_loaded_ws_edges):
         
     | 
| 58 | 
         
            -
                        assert edge.id == loaded_edge.id
         
     | 
| 59 | 
         
            -
                        assert edge.source == loaded_edge.source
         
     | 
| 60 | 
         
            -
                        assert edge.target == loaded_edge.target
         
     | 
| 61 | 
         
            -
                        assert edge.sourceHandle == loaded_edge.sourceHandle
         
     | 
| 62 | 
         
            -
                        assert edge.targetHandle == loaded_edge.targetHandle
         
     | 
| 63 | 
         
            -
                finally:
         
     | 
| 64 | 
         
            -
                    os.remove(path)
         
     | 
| 65 | 
         
            -
             
     | 
| 66 | 
         
            -
             
     | 
| 67 | 
         
            -
            @pytest.fixture(scope="session", autouse=True)
         
     | 
| 68 | 
         
            -
            def populate_ops_catalog():
         
     | 
| 69 | 
         
            -
                ops.register_passive_op(
         
     | 
| 70 | 
         
            -
                    env="test",
         
     | 
| 71 | 
         
            -
                    name="Test Operation",
         
     | 
| 72 | 
         
            -
                    inputs=[],
         
     | 
| 73 | 
         
            -
                    params=[
         
     | 
| 74 | 
         
            -
                        ops.Parameter(name="param_int", default=1),
         
     | 
| 75 | 
         
            -
                        ops.Parameter(name="param_str", default="test"),
         
     | 
| 76 | 
         
            -
                    ],
         
     | 
| 77 | 
         
            -
                )
         
     | 
| 78 | 
         
            -
             
     | 
| 79 | 
         
            -
             
     | 
| 80 | 
         
            -
            def test_update_metadata():
         
     | 
| 81 | 
         
            -
                ws = workspace.Workspace(env="test")
         
     | 
| 82 | 
         
            -
                ws.nodes.append(
         
     | 
| 83 | 
         
            -
                    workspace.WorkspaceNode(
         
     | 
| 84 | 
         
            -
                        id="1",
         
     | 
| 85 | 
         
            -
                        type="basic",
         
     | 
| 86 | 
         
            -
                        data=workspace.WorkspaceNodeData(
         
     | 
| 87 | 
         
            -
                            title="Test Operation", params={"param_int": 1}
         
     | 
| 88 | 
         
            -
                        ),
         
     | 
| 89 | 
         
            -
                        position=workspace.NodePosition(x=0, y=0),
         
     | 
| 90 | 
         
            -
                    )
         
     | 
| 91 | 
         
            -
                )
         
     | 
| 92 | 
         
            -
                ws.nodes.append(
         
     | 
| 93 | 
         
            -
                    workspace.WorkspaceNode(
         
     | 
| 94 | 
         
            -
                        id="2",
         
     | 
| 95 | 
         
            -
                        type="basic",
         
     | 
| 96 | 
         
            -
                        data=workspace.WorkspaceNodeData(title="Unknown Operation", params={}),
         
     | 
| 97 | 
         
            -
                        position=workspace.NodePosition(x=0, y=0),
         
     | 
| 98 | 
         
            -
                    )
         
     | 
| 99 | 
         
            -
                )
         
     | 
| 100 | 
         
            -
                updated_ws = workspace._update_metadata(ws)
         
     | 
| 101 | 
         
            -
                assert updated_ws.nodes[0].data.meta.name == "Test Operation"
         
     | 
| 102 | 
         
            -
                assert updated_ws.nodes[0].data.error is None
         
     | 
| 103 | 
         
            -
                assert updated_ws.nodes[0].data.params == {"param_int": 1}
         
     | 
| 104 | 
         
            -
                assert updated_ws.nodes[0].data.meta.params == {
         
     | 
| 105 | 
         
            -
                    "param_int": ops.Parameter(name="param_int", default=1),
         
     | 
| 106 | 
         
            -
                    "param_str": ops.Parameter(name="param_str", default="test"),
         
     | 
| 107 | 
         
            -
                }
         
     | 
| 108 | 
         
            -
                assert not hasattr(updated_ws.nodes[1].data, "meta")
         
     | 
| 109 | 
         
            -
                assert updated_ws.nodes[1].data.error == "Unknown operation."
         
     | 
| 110 | 
         
            -
             
     | 
| 111 | 
         
            -
             
     | 
| 112 | 
         
            -
            def test_update_metadata_with_empty_workspace():
         
     | 
| 113 | 
         
            -
                ws = workspace.Workspace(env="test")
         
     | 
| 114 | 
         
            -
                updated_ws = workspace._update_metadata(ws)
         
     | 
| 115 | 
         
            -
                assert len(updated_ws.nodes) == 0
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
    	
        lynxkite-graph-analytics/src/lynxkite_plugins/graph_analytics/lynxkite_ops.py
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 1 | 
         
             
            """Graph analytics operations. To be split into separate files when we have more."""
         
     | 
| 2 | 
         | 
| 3 | 
         
             
            import os
         
     | 
| 4 | 
         
            -
            from lynxkite.core import ops 
     | 
| 5 | 
         
             
            from collections import deque
         
     | 
| 6 | 
         
             
            import dataclasses
         
     | 
| 7 | 
         
             
            import functools
         
     | 
| 
         @@ -34,7 +34,6 @@ class RelationDefinition: 
     | 
|
| 34 | 
         
             
                target_key: str  # The column in the target table that contains the node ID.
         
     | 
| 35 | 
         | 
| 36 | 
         | 
| 37 | 
         
            -
            # TODO: Convert this to Pydantic
         
     | 
| 38 | 
         
             
            @dataclasses.dataclass
         
     | 
| 39 | 
         
             
            class Bundle:
         
     | 
| 40 | 
         
             
                """A collection of DataFrames and other data.
         
     | 
| 
         @@ -116,50 +115,32 @@ def disambiguate_edges(ws): 
     | 
|
| 116 | 
         | 
| 117 | 
         | 
| 118 | 
         
             
            @ops.register_executor(ENV)
         
     | 
| 119 | 
         
            -
            async def execute(ws 
     | 
| 120 | 
         
            -
                catalog 
     | 
| 121 | 
         
             
                disambiguate_edges(ws)
         
     | 
| 122 | 
         
            -
                 
     | 
| 123 | 
         
             
                failed = 0
         
     | 
| 124 | 
         
            -
                while len( 
     | 
| 125 | 
         
             
                    for node in ws.nodes:
         
     | 
| 126 | 
         
            -
                        if node.id in  
     | 
| 127 | 
         
             
                            continue
         
     | 
| 128 | 
         
             
                        # TODO: Take the input/output handles into account.
         
     | 
| 129 | 
         
            -
                         
     | 
| 130 | 
         
            -
             
     | 
| 131 | 
         
            -
             
     | 
| 132 | 
         
            -
                        if all(input in computed_outputs for input in operation_inputs):
         
     | 
| 133 | 
         
            -
                            # All inputs for this node are ready, we can compute the output.
         
     | 
| 134 | 
         
            -
                            operation_inputs = [
         
     | 
| 135 | 
         
            -
                                computed_outputs[input] for input in operation_inputs
         
     | 
| 136 | 
         
            -
                            ]
         
     | 
| 137 | 
         
             
                            data = node.data
         
     | 
| 
         | 
|
| 138 | 
         
             
                            params = {**data.params}
         
     | 
| 
         | 
|
| 139 | 
         
             
                            try:
         
     | 
| 140 | 
         
            -
                                 
     | 
| 141 | 
         
            -
             
     | 
| 142 | 
         
            -
             
     | 
| 143 | 
         
            -
                                     
     | 
| 144 | 
         
            -
             
     | 
| 145 | 
         
            -
                                     
     | 
| 146 | 
         
            -
                                         
     | 
| 147 | 
         
            -
             
     | 
| 148 | 
         
            -
                                        operation_inputs[i] = input_value.to_nx()
         
     | 
| 149 | 
         
            -
                                    elif input_signature.type == Bundle and isinstance(
         
     | 
| 150 | 
         
            -
                                        input_value, nx.Graph
         
     | 
| 151 | 
         
            -
                                    ):
         
     | 
| 152 | 
         
            -
                                        operation_inputs[i] = Bundle.from_nx(input_value)
         
     | 
| 153 | 
         
            -
                                    elif input_signature.type == Bundle and isinstance(
         
     | 
| 154 | 
         
            -
                                        input_value, pd.DataFrame
         
     | 
| 155 | 
         
            -
                                    ):
         
     | 
| 156 | 
         
            -
                                        operation_inputs[i] = Bundle.from_df(input_value)
         
     | 
| 157 | 
         
            -
                                output = op(*operation_inputs, **params)
         
     | 
| 158 | 
         
            -
                            except KeyError:
         
     | 
| 159 | 
         
            -
                                traceback.print_exc()
         
     | 
| 160 | 
         
            -
                                data.error = "Operation not found in catalog"
         
     | 
| 161 | 
         
            -
                                failed += 1
         
     | 
| 162 | 
         
            -
                                continue
         
     | 
| 163 | 
         
             
                            except Exception as e:
         
     | 
| 164 | 
         
             
                                traceback.print_exc()
         
     | 
| 165 | 
         
             
                                data.error = str(e)
         
     | 
| 
         @@ -167,16 +148,13 @@ async def execute(ws: workspace.Workspace): 
     | 
|
| 167 | 
         
             
                                continue
         
     | 
| 168 | 
         
             
                            if len(op.inputs) == 1 and op.inputs.get("multi") == "*":
         
     | 
| 169 | 
         
             
                                # It's a flexible input. Create n+1 handles.
         
     | 
| 170 | 
         
            -
                                 
     | 
| 171 | 
         
            -
                                data.inputs = {
         
     | 
| 172 | 
         
            -
                                    f"input{i}": None for i in range(len(operation_inputs) + 1)
         
     | 
| 173 | 
         
            -
                                }
         
     | 
| 174 | 
         
             
                            data.error = None
         
     | 
| 175 | 
         
            -
                             
     | 
| 176 | 
         
             
                            if (
         
     | 
| 177 | 
         
            -
                                op. 
     | 
| 178 | 
         
            -
                                or op. 
     | 
| 179 | 
         
            -
                                or op. 
     | 
| 180 | 
         
             
                            ):
         
     | 
| 181 | 
         
             
                                data.display = output
         
     | 
| 182 | 
         | 
| 
         @@ -210,7 +188,6 @@ def create_scale_free_graph(*, nodes: int = 10): 
     | 
|
| 210 | 
         
             
            @op("Compute PageRank")
         
     | 
| 211 | 
         
             
            @nx_node_attribute_func("pagerank")
         
     | 
| 212 | 
         
             
            def compute_pagerank(graph: nx.Graph, *, damping=0.85, iterations=100):
         
     | 
| 213 | 
         
            -
                # TODO: This requires scipy to be installed.
         
     | 
| 214 | 
         
             
                return nx.pagerank(graph, alpha=damping, max_iter=iterations)
         
     | 
| 215 | 
         | 
| 216 | 
         | 
| 
         @@ -304,7 +281,7 @@ def _map_color(value): 
     | 
|
| 304 | 
         
             
                    ]
         
     | 
| 305 | 
         | 
| 306 | 
         | 
| 307 | 
         
            -
            @op("Visualize graph", view= 
     | 
| 308 | 
         
             
            def visualize_graph(graph: Bundle, *, color_nodes_by: ops.NodeAttribute = None):
         
     | 
| 309 | 
         
             
                nodes = graph.dfs["nodes"].copy()
         
     | 
| 310 | 
         
             
                if color_nodes_by:
         
     | 
| 
         @@ -358,7 +335,7 @@ def collect(df: pd.DataFrame): 
     | 
|
| 358 | 
         
             
                return df.values.tolist()
         
     | 
| 359 | 
         | 
| 360 | 
         | 
| 361 | 
         
            -
            @op("View tables", view= 
     | 
| 362 | 
         
             
            def view_tables(bundle: Bundle):
         
     | 
| 363 | 
         
             
                v = {
         
     | 
| 364 | 
         
             
                    "dataframes": {
         
     | 
| 
         | 
|
| 1 | 
         
             
            """Graph analytics operations. To be split into separate files when we have more."""
         
     | 
| 2 | 
         | 
| 3 | 
         
             
            import os
         
     | 
| 4 | 
         
            +
            from lynxkite.core import ops
         
     | 
| 5 | 
         
             
            from collections import deque
         
     | 
| 6 | 
         
             
            import dataclasses
         
     | 
| 7 | 
         
             
            import functools
         
     | 
| 
         | 
|
| 34 | 
         
             
                target_key: str  # The column in the target table that contains the node ID.
         
     | 
| 35 | 
         | 
| 36 | 
         | 
| 
         | 
|
| 37 | 
         
             
            @dataclasses.dataclass
         
     | 
| 38 | 
         
             
            class Bundle:
         
     | 
| 39 | 
         
             
                """A collection of DataFrames and other data.
         
     | 
| 
         | 
|
| 115 | 
         | 
| 116 | 
         | 
| 117 | 
         
             
            @ops.register_executor(ENV)
         
     | 
| 118 | 
         
            +
            async def execute(ws):
         
     | 
| 119 | 
         
            +
                catalog = ops.CATALOGS[ENV]
         
     | 
| 120 | 
         
             
                disambiguate_edges(ws)
         
     | 
| 121 | 
         
            +
                outputs = {}
         
     | 
| 122 | 
         
             
                failed = 0
         
     | 
| 123 | 
         
            +
                while len(outputs) + failed < len(ws.nodes):
         
     | 
| 124 | 
         
             
                    for node in ws.nodes:
         
     | 
| 125 | 
         
            +
                        if node.id in outputs:
         
     | 
| 126 | 
         
             
                            continue
         
     | 
| 127 | 
         
             
                        # TODO: Take the input/output handles into account.
         
     | 
| 128 | 
         
            +
                        inputs = [edge.source for edge in ws.edges if edge.target == node.id]
         
     | 
| 129 | 
         
            +
                        if all(input in outputs for input in inputs):
         
     | 
| 130 | 
         
            +
                            inputs = [outputs[input] for input in inputs]
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 131 | 
         
             
                            data = node.data
         
     | 
| 132 | 
         
            +
                            op = catalog[data.title]
         
     | 
| 133 | 
         
             
                            params = {**data.params}
         
     | 
| 134 | 
         
            +
                            # Convert inputs.
         
     | 
| 135 | 
         
             
                            try:
         
     | 
| 136 | 
         
            +
                                for i, (x, p) in enumerate(zip(inputs, op.inputs.values())):
         
     | 
| 137 | 
         
            +
                                    if p.type == nx.Graph and isinstance(x, Bundle):
         
     | 
| 138 | 
         
            +
                                        inputs[i] = x.to_nx()
         
     | 
| 139 | 
         
            +
                                    elif p.type == Bundle and isinstance(x, nx.Graph):
         
     | 
| 140 | 
         
            +
                                        inputs[i] = Bundle.from_nx(x)
         
     | 
| 141 | 
         
            +
                                    elif p.type == Bundle and isinstance(x, pd.DataFrame):
         
     | 
| 142 | 
         
            +
                                        inputs[i] = Bundle.from_df(x)
         
     | 
| 143 | 
         
            +
                                output = op(*inputs, **params)
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 144 | 
         
             
                            except Exception as e:
         
     | 
| 145 | 
         
             
                                traceback.print_exc()
         
     | 
| 146 | 
         
             
                                data.error = str(e)
         
     | 
| 
         | 
|
| 148 | 
         
             
                                continue
         
     | 
| 149 | 
         
             
                            if len(op.inputs) == 1 and op.inputs.get("multi") == "*":
         
     | 
| 150 | 
         
             
                                # It's a flexible input. Create n+1 handles.
         
     | 
| 151 | 
         
            +
                                data.inputs = {f"input{i}": None for i in range(len(inputs) + 1)}
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 152 | 
         
             
                            data.error = None
         
     | 
| 153 | 
         
            +
                            outputs[node.id] = output
         
     | 
| 154 | 
         
             
                            if (
         
     | 
| 155 | 
         
            +
                                op.type == "visualization"
         
     | 
| 156 | 
         
            +
                                or op.type == "table_view"
         
     | 
| 157 | 
         
            +
                                or op.type == "image"
         
     | 
| 158 | 
         
             
                            ):
         
     | 
| 159 | 
         
             
                                data.display = output
         
     | 
| 160 | 
         | 
| 
         | 
|
| 188 | 
         
             
            @op("Compute PageRank")
         
     | 
| 189 | 
         
             
            @nx_node_attribute_func("pagerank")
         
     | 
| 190 | 
         
             
            def compute_pagerank(graph: nx.Graph, *, damping=0.85, iterations=100):
         
     | 
| 
         | 
|
| 191 | 
         
             
                return nx.pagerank(graph, alpha=damping, max_iter=iterations)
         
     | 
| 192 | 
         | 
| 193 | 
         | 
| 
         | 
|
| 281 | 
         
             
                    ]
         
     | 
| 282 | 
         | 
| 283 | 
         | 
| 284 | 
         
            +
            @op("Visualize graph", view="visualization")
         
     | 
| 285 | 
         
             
            def visualize_graph(graph: Bundle, *, color_nodes_by: ops.NodeAttribute = None):
         
     | 
| 286 | 
         
             
                nodes = graph.dfs["nodes"].copy()
         
     | 
| 287 | 
         
             
                if color_nodes_by:
         
     | 
| 
         | 
|
| 335 | 
         
             
                return df.values.tolist()
         
     | 
| 336 | 
         | 
| 337 | 
         | 
| 338 | 
         
            +
            @op("View tables", view="table_view")
         
     | 
| 339 | 
         
             
            def view_tables(bundle: Bundle):
         
     | 
| 340 | 
         
             
                v = {
         
     | 
| 341 | 
         
             
                    "dataframes": {
         
     | 
    	
        lynxkite-graph-analytics/src/lynxkite_plugins/graph_analytics/networkx_ops.py
    CHANGED
    
    | 
         @@ -54,7 +54,7 @@ def register_networkx(env: str): 
     | 
|
| 54 | 
         
             
                            params=params,
         
     | 
| 55 | 
         
             
                            inputs=inputs,
         
     | 
| 56 | 
         
             
                            outputs={"output": ops.Output(name="output", type=nx.Graph)},
         
     | 
| 57 | 
         
            -
                             
     | 
| 58 | 
         
             
                        )
         
     | 
| 59 | 
         
             
                        cat[name] = op
         
     | 
| 60 | 
         | 
| 
         | 
|
| 54 | 
         
             
                            params=params,
         
     | 
| 55 | 
         
             
                            inputs=inputs,
         
     | 
| 56 | 
         
             
                            outputs={"output": ops.Output(name="output", type=nx.Graph)},
         
     | 
| 57 | 
         
            +
                            type="basic",
         
     | 
| 58 | 
         
             
                        )
         
     | 
| 59 | 
         
             
                        cat[name] = op
         
     | 
| 60 | 
         | 
    	
        lynxkite-graph-analytics/src/lynxkite_plugins/graph_analytics/pytorch_model_ops.py
    CHANGED
    
    | 
         @@ -13,10 +13,10 @@ def reg(name, inputs=[], outputs=None, params=[]): 
     | 
|
| 13 | 
         
             
                    ENV,
         
     | 
| 14 | 
         
             
                    name,
         
     | 
| 15 | 
         
             
                    inputs=[
         
     | 
| 16 | 
         
            -
                        ops.Input(name=name,  
     | 
| 17 | 
         
             
                    ],
         
     | 
| 18 | 
         
             
                    outputs=[
         
     | 
| 19 | 
         
            -
                        ops.Output(name=name,  
     | 
| 20 | 
         
             
                    ],
         
     | 
| 21 | 
         
             
                    params=params,
         
     | 
| 22 | 
         
             
                )
         
     | 
| 
         @@ -64,4 +64,6 @@ reg( 
     | 
|
| 64 | 
         
             
                    ),
         
     | 
| 65 | 
         
             
                    P.basic("lr", 0.001),
         
     | 
| 66 | 
         
             
                ],
         
     | 
| 67 | 
         
            -
            )
         
     | 
| 
         | 
|
| 
         | 
| 
         | 
|
| 13 | 
         
             
                    ENV,
         
     | 
| 14 | 
         
             
                    name,
         
     | 
| 15 | 
         
             
                    inputs=[
         
     | 
| 16 | 
         
            +
                        ops.Input(name=name, position="bottom", type="tensor") for name in inputs
         
     | 
| 17 | 
         
             
                    ],
         
     | 
| 18 | 
         
             
                    outputs=[
         
     | 
| 19 | 
         
            +
                        ops.Output(name=name, position="top", type="tensor") for name in outputs
         
     | 
| 20 | 
         
             
                    ],
         
     | 
| 21 | 
         
             
                    params=params,
         
     | 
| 22 | 
         
             
                )
         
     | 
| 
         | 
|
| 64 | 
         
             
                    ),
         
     | 
| 65 | 
         
             
                    P.basic("lr", 0.001),
         
     | 
| 66 | 
         
             
                ],
         
     | 
| 67 | 
         
            +
            )
         
     | 
| 68 | 
         
            +
             
     | 
| 69 | 
         
            +
            ops.register_area(ENV, "Repeat", params=[ops.Parameter.basic("times", 1, int)])
         
     | 
    	
        lynxkite-graph-analytics/tests/test_lynxkite_ops.py
    DELETED
    
    | 
         @@ -1,100 +0,0 @@ 
     | 
|
| 1 | 
         
            -
            import sys
         
     | 
| 2 | 
         
            -
             
     | 
| 3 | 
         
            -
            # Add the project root to sys.path
         
     | 
| 4 | 
         
            -
            sys.path.insert(0, "/home/chema/work/lynxkite-2024/lynxkite-graph-analytics")
         
     | 
| 5 | 
         
            -
            import pandas as pd
         
     | 
| 6 | 
         
            -
            import pytest
         
     | 
| 7 | 
         
            -
            import networkx as nx
         
     | 
| 8 | 
         
            -
             
     | 
| 9 | 
         
            -
            from lynxkite.core import workspace
         
     | 
| 10 | 
         
            -
             
     | 
| 11 | 
         
            -
            # from lynxkite_plugins.graph_analytics.lynxkite_ops import execute
         
     | 
| 12 | 
         
            -
            from src.lynxkite_plugins.graph_analytics.lynxkite_ops import Bundle, execute, op
         
     | 
| 13 | 
         
            -
             
     | 
| 14 | 
         
            -
             
     | 
| 15 | 
         
            -
            async def test_execute_operation_not_in_catalog():
         
     | 
| 16 | 
         
            -
                ws = workspace.Workspace(env="test")
         
     | 
| 17 | 
         
            -
                ws.nodes.append(
         
     | 
| 18 | 
         
            -
                    workspace.WorkspaceNode(
         
     | 
| 19 | 
         
            -
                        id="1",
         
     | 
| 20 | 
         
            -
                        type="node_type",
         
     | 
| 21 | 
         
            -
                        data=workspace.WorkspaceNodeData(title="Non existing op", params={}),
         
     | 
| 22 | 
         
            -
                        position=workspace.NodePosition(x=0, y=0),
         
     | 
| 23 | 
         
            -
                    )
         
     | 
| 24 | 
         
            -
                )
         
     | 
| 25 | 
         
            -
                await execute(ws)
         
     | 
| 26 | 
         
            -
                assert ws.nodes[0].data.error == "Operation not found in catalog"
         
     | 
| 27 | 
         
            -
             
     | 
| 28 | 
         
            -
             
     | 
| 29 | 
         
            -
            async def test_execute_operation_inputs_correct_cast():
         
     | 
| 30 | 
         
            -
                # Test that the automatic casting of operation inputs works correctly.
         
     | 
| 31 | 
         
            -
             
     | 
| 32 | 
         
            -
                @op("Create Bundle")
         
     | 
| 33 | 
         
            -
                def create_bundle() -> Bundle:
         
     | 
| 34 | 
         
            -
                    df = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]})
         
     | 
| 35 | 
         
            -
                    return Bundle(dfs={"edges": df})
         
     | 
| 36 | 
         
            -
             
     | 
| 37 | 
         
            -
                @op("Bundle to Graph")
         
     | 
| 38 | 
         
            -
                def bundle_to_graph(graph: nx.Graph) -> nx.Graph:
         
     | 
| 39 | 
         
            -
                    return graph
         
     | 
| 40 | 
         
            -
             
     | 
| 41 | 
         
            -
                @op("Graph to Bundle")
         
     | 
| 42 | 
         
            -
                def graph_to_bundle(bundle: Bundle) -> pd.DataFrame:
         
     | 
| 43 | 
         
            -
                    return list(bundle.dfs.values())[0]
         
     | 
| 44 | 
         
            -
             
     | 
| 45 | 
         
            -
                @op("Dataframe to Bundle")
         
     | 
| 46 | 
         
            -
                def dataframe_to_bundle(bundle: Bundle) -> Bundle:
         
     | 
| 47 | 
         
            -
                    return bundle
         
     | 
| 48 | 
         
            -
             
     | 
| 49 | 
         
            -
                ws = workspace.Workspace(env="test")
         
     | 
| 50 | 
         
            -
                ws.nodes.append(
         
     | 
| 51 | 
         
            -
                    workspace.WorkspaceNode(
         
     | 
| 52 | 
         
            -
                        id="1",
         
     | 
| 53 | 
         
            -
                        type="node_type",
         
     | 
| 54 | 
         
            -
                        data=workspace.WorkspaceNodeData(title="Create Bundle", params={}),
         
     | 
| 55 | 
         
            -
                        position=workspace.NodePosition(x=0, y=0),
         
     | 
| 56 | 
         
            -
                    )
         
     | 
| 57 | 
         
            -
                )
         
     | 
| 58 | 
         
            -
                ws.nodes.append(
         
     | 
| 59 | 
         
            -
                    workspace.WorkspaceNode(
         
     | 
| 60 | 
         
            -
                        id="2",
         
     | 
| 61 | 
         
            -
                        type="node_type",
         
     | 
| 62 | 
         
            -
                        data=workspace.WorkspaceNodeData(title="Bundle to Graph", params={}),
         
     | 
| 63 | 
         
            -
                        position=workspace.NodePosition(x=100, y=0),
         
     | 
| 64 | 
         
            -
                    )
         
     | 
| 65 | 
         
            -
                )
         
     | 
| 66 | 
         
            -
                ws.nodes.append(
         
     | 
| 67 | 
         
            -
                    workspace.WorkspaceNode(
         
     | 
| 68 | 
         
            -
                        id="3",
         
     | 
| 69 | 
         
            -
                        type="node_type",
         
     | 
| 70 | 
         
            -
                        data=workspace.WorkspaceNodeData(title="Graph to Bundle", params={}),
         
     | 
| 71 | 
         
            -
                        position=workspace.NodePosition(x=200, y=0),
         
     | 
| 72 | 
         
            -
                    )
         
     | 
| 73 | 
         
            -
                )
         
     | 
| 74 | 
         
            -
                ws.nodes.append(
         
     | 
| 75 | 
         
            -
                    workspace.WorkspaceNode(
         
     | 
| 76 | 
         
            -
                        id="4",
         
     | 
| 77 | 
         
            -
                        type="node_type",
         
     | 
| 78 | 
         
            -
                        data=workspace.WorkspaceNodeData(title="Dataframe to Bundle", params={}),
         
     | 
| 79 | 
         
            -
                        position=workspace.NodePosition(x=300, y=0),
         
     | 
| 80 | 
         
            -
                    )
         
     | 
| 81 | 
         
            -
                )
         
     | 
| 82 | 
         
            -
                ws.edges = [
         
     | 
| 83 | 
         
            -
                    workspace.WorkspaceEdge(
         
     | 
| 84 | 
         
            -
                        id="1", source="1", target="2", sourceHandle="1", targetHandle="2"
         
     | 
| 85 | 
         
            -
                    ),
         
     | 
| 86 | 
         
            -
                    workspace.WorkspaceEdge(
         
     | 
| 87 | 
         
            -
                        id="2", source="2", target="3", sourceHandle="2", targetHandle="3"
         
     | 
| 88 | 
         
            -
                    ),
         
     | 
| 89 | 
         
            -
                    workspace.WorkspaceEdge(
         
     | 
| 90 | 
         
            -
                        id="3", source="3", target="4", sourceHandle="3", targetHandle="4"
         
     | 
| 91 | 
         
            -
                    ),
         
     | 
| 92 | 
         
            -
                ]
         
     | 
| 93 | 
         
            -
             
     | 
| 94 | 
         
            -
                await execute(ws)
         
     | 
| 95 | 
         
            -
             
     | 
| 96 | 
         
            -
                assert all([node.data.error is None for node in ws.nodes])
         
     | 
| 97 | 
         
            -
             
     | 
| 98 | 
         
            -
             
     | 
| 99 | 
         
            -
            if __name__ == "__main__":
         
     | 
| 100 | 
         
            -
                pytest.main()
         
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
    	
        lynxkite-lynxscribe/src/lynxkite_plugins/lynxscribe/llm_ops.py
    CHANGED
    
    | 
         @@ -68,13 +68,13 @@ def split_document(input, *, delimiter: str = "\\n\\n"): 
     | 
|
| 68 | 
         
             
                return pd.DataFrame(chunks, columns=["text"])
         
     | 
| 69 | 
         | 
| 70 | 
         | 
| 71 | 
         
            -
            @ops. 
     | 
| 72 | 
         
             
            @op("Build document graph")
         
     | 
| 73 | 
         
             
            def build_document_graph(input):
         
     | 
| 74 | 
         
             
                return [{"source": i, "target": i + 1} for i in range(len(input) - 1)]
         
     | 
| 75 | 
         | 
| 76 | 
         | 
| 77 | 
         
            -
            @ops. 
     | 
| 78 | 
         
             
            @op("Predict links")
         
     | 
| 79 | 
         
             
            def predict_links(nodes, edges):
         
     | 
| 80 | 
         
             
                """A placeholder for a real algorithm. For now just adds 2-hop neighbors."""
         
     | 
| 
         @@ -89,7 +89,7 @@ def predict_links(nodes, edges): 
     | 
|
| 89 | 
         
             
                return edges + new_edges
         
     | 
| 90 | 
         | 
| 91 | 
         | 
| 92 | 
         
            -
            @ops. 
     | 
| 93 | 
         
             
            @op("Add neighbors")
         
     | 
| 94 | 
         
             
            def add_neighbors(nodes, edges, item):
         
     | 
| 95 | 
         
             
                nodes = pd.DataFrame(nodes)
         
     | 
| 
         @@ -133,7 +133,7 @@ def ask_llm(input, *, model: str, accepted_regex: str = None, max_tokens: int = 
     | 
|
| 133 | 
         
             
                return [{**input, "response": r} for r in results]
         
     | 
| 134 | 
         | 
| 135 | 
         | 
| 136 | 
         
            -
            @op("View", view= 
     | 
| 137 | 
         
             
            def view(input, *, _ctx: one_by_one.Context):
         
     | 
| 138 | 
         
             
                v = _ctx.last_result
         
     | 
| 139 | 
         
             
                if v:
         
     | 
| 
         @@ -152,8 +152,8 @@ def view(input, *, _ctx: one_by_one.Context): 
     | 
|
| 152 | 
         
             
                return v
         
     | 
| 153 | 
         | 
| 154 | 
         | 
| 155 | 
         
            -
            @ops. 
     | 
| 156 | 
         
            -
            @ops. 
     | 
| 157 | 
         
             
            @op("Loop")
         
     | 
| 158 | 
         
             
            def loop(input, *, max_iterations: int = 3, _ctx: one_by_one.Context):
         
     | 
| 159 | 
         
             
                """Data can flow back here max_iterations-1 times."""
         
     | 
| 
         @@ -174,7 +174,7 @@ class RagEngine(enum.Enum): 
     | 
|
| 174 | 
         
             
                Custom = "Custom"
         
     | 
| 175 | 
         | 
| 176 | 
         | 
| 177 | 
         
            -
            @ops. 
     | 
| 178 | 
         
             
            @op("RAG")
         
     | 
| 179 | 
         
             
            def rag(
         
     | 
| 180 | 
         
             
                input,
         
     | 
| 
         | 
|
| 68 | 
         
             
                return pd.DataFrame(chunks, columns=["text"])
         
     | 
| 69 | 
         | 
| 70 | 
         | 
| 71 | 
         
            +
            @ops.input_position(input="top")
         
     | 
| 72 | 
         
             
            @op("Build document graph")
         
     | 
| 73 | 
         
             
            def build_document_graph(input):
         
     | 
| 74 | 
         
             
                return [{"source": i, "target": i + 1} for i in range(len(input) - 1)]
         
     | 
| 75 | 
         | 
| 76 | 
         | 
| 77 | 
         
            +
            @ops.input_position(nodes="top", edges="top")
         
     | 
| 78 | 
         
             
            @op("Predict links")
         
     | 
| 79 | 
         
             
            def predict_links(nodes, edges):
         
     | 
| 80 | 
         
             
                """A placeholder for a real algorithm. For now just adds 2-hop neighbors."""
         
     | 
| 
         | 
|
| 89 | 
         
             
                return edges + new_edges
         
     | 
| 90 | 
         | 
| 91 | 
         | 
| 92 | 
         
            +
            @ops.input_position(nodes="top", edges="top")
         
     | 
| 93 | 
         
             
            @op("Add neighbors")
         
     | 
| 94 | 
         
             
            def add_neighbors(nodes, edges, item):
         
     | 
| 95 | 
         
             
                nodes = pd.DataFrame(nodes)
         
     | 
| 
         | 
|
| 133 | 
         
             
                return [{**input, "response": r} for r in results]
         
     | 
| 134 | 
         | 
| 135 | 
         | 
| 136 | 
         
            +
            @op("View", view="table_view")
         
     | 
| 137 | 
         
             
            def view(input, *, _ctx: one_by_one.Context):
         
     | 
| 138 | 
         
             
                v = _ctx.last_result
         
     | 
| 139 | 
         
             
                if v:
         
     | 
| 
         | 
|
| 152 | 
         
             
                return v
         
     | 
| 153 | 
         | 
| 154 | 
         | 
| 155 | 
         
            +
            @ops.input_position(input="right")
         
     | 
| 156 | 
         
            +
            @ops.output_position(output="left")
         
     | 
| 157 | 
         
             
            @op("Loop")
         
     | 
| 158 | 
         
             
            def loop(input, *, max_iterations: int = 3, _ctx: one_by_one.Context):
         
     | 
| 159 | 
         
             
                """Data can flow back here max_iterations-1 times."""
         
     | 
| 
         | 
|
| 174 | 
         
             
                Custom = "Custom"
         
     | 
| 175 | 
         | 
| 176 | 
         | 
| 177 | 
         
            +
            @ops.input_position(db="top")
         
     | 
| 178 | 
         
             
            @op("RAG")
         
     | 
| 179 | 
         
             
            def rag(
         
     | 
| 180 | 
         
             
                input,
         
     | 
    	
        lynxkite-lynxscribe/src/lynxkite_plugins/lynxscribe/lynxscribe_ops.py
    CHANGED
    
    | 
         @@ -24,7 +24,7 @@ from lynxkite.core.executors import one_by_one 
     | 
|
| 24 | 
         
             
            ENV = "LynxScribe"
         
     | 
| 25 | 
         
             
            one_by_one.register(ENV)
         
     | 
| 26 | 
         
             
            op = ops.op_registration(ENV)
         
     | 
| 27 | 
         
            -
            output_on_top = ops. 
     | 
| 28 | 
         | 
| 29 | 
         | 
| 30 | 
         
             
            @output_on_top
         
     | 
| 
         @@ -42,7 +42,7 @@ def llm(*, name="openai"): 
     | 
|
| 42 | 
         | 
| 43 | 
         | 
| 44 | 
         
             
            @output_on_top
         
     | 
| 45 | 
         
            -
            @ops. 
     | 
| 46 | 
         
             
            @op("Text embedder")
         
     | 
| 47 | 
         
             
            def text_embedder(llm, *, model="text-embedding-ada-002"):
         
     | 
| 48 | 
         
             
                llm = llm[0]["llm"]
         
     | 
| 
         @@ -51,7 +51,7 @@ def text_embedder(llm, *, model="text-embedding-ada-002"): 
     | 
|
| 51 | 
         | 
| 52 | 
         | 
| 53 | 
         
             
            @output_on_top
         
     | 
| 54 | 
         
            -
            @ops. 
     | 
| 55 | 
         
             
            @op("RAG graph")
         
     | 
| 56 | 
         
             
            def rag_graph(vector_store, text_embedder):
         
     | 
| 57 | 
         
             
                vector_store = vector_store[0]["vector_store"]
         
     | 
| 
         @@ -78,7 +78,7 @@ DEFAULT_NEGATIVE_ANSWER = "I'm sorry, but the data I've been trained on does not 
     | 
|
| 78 | 
         | 
| 79 | 
         | 
| 80 | 
         
             
            @output_on_top
         
     | 
| 81 | 
         
            -
            @ops. 
     | 
| 82 | 
         
             
            @op("RAG chatbot")
         
     | 
| 83 | 
         
             
            def rag_chatbot(
         
     | 
| 84 | 
         
             
                rag_graph,
         
     | 
| 
         @@ -107,7 +107,7 @@ def rag_chatbot( 
     | 
|
| 107 | 
         | 
| 108 | 
         | 
| 109 | 
         
             
            @output_on_top
         
     | 
| 110 | 
         
            -
            @ops. 
     | 
| 111 | 
         
             
            @op("Chat processor")
         
     | 
| 112 | 
         
             
            def chat_processor(processor, *, _ctx: one_by_one.Context):
         
     | 
| 113 | 
         
             
                cfg = _ctx.last_result or {
         
     | 
| 
         @@ -152,7 +152,7 @@ def mask(*, name="", regex="", exceptions="", mask_pattern=""): 
     | 
|
| 152 | 
         
             
                }
         
     | 
| 153 | 
         | 
| 154 | 
         | 
| 155 | 
         
            -
            @ops. 
     | 
| 156 | 
         
             
            @op("Test Chat API")
         
     | 
| 157 | 
         
             
            async def test_chat_api(message, chat_api, *, show_details=False):
         
     | 
| 158 | 
         
             
                chat_api = chat_api[0]["chat_api"]
         
     | 
| 
         @@ -173,7 +173,7 @@ def input_chat(*, chat: str): 
     | 
|
| 173 | 
         | 
| 174 | 
         | 
| 175 | 
         
             
            @output_on_top
         
     | 
| 176 | 
         
            -
            @ops. 
     | 
| 177 | 
         
             
            @op("Chat API")
         
     | 
| 178 | 
         
             
            def chat_api(chatbot, chat_processor, knowledge_base, *, model="gpt-4o-mini"):
         
     | 
| 179 | 
         
             
                chatbot = chatbot[0]["chatbot"]
         
     | 
| 
         @@ -205,7 +205,7 @@ def knowledge_base( 
     | 
|
| 205 | 
         
             
                }
         
     | 
| 206 | 
         | 
| 207 | 
         | 
| 208 | 
         
            -
            @op("View", view= 
     | 
| 209 | 
         
             
            def view(input):
         
     | 
| 210 | 
         
             
                columns = [str(c) for c in input.keys() if not str(c).startswith("_")]
         
     | 
| 211 | 
         
             
                v = {
         
     | 
| 
         | 
|
| 24 | 
         
             
            ENV = "LynxScribe"
         
     | 
| 25 | 
         
             
            one_by_one.register(ENV)
         
     | 
| 26 | 
         
             
            op = ops.op_registration(ENV)
         
     | 
| 27 | 
         
            +
            output_on_top = ops.output_position(output="top")
         
     | 
| 28 | 
         | 
| 29 | 
         | 
| 30 | 
         
             
            @output_on_top
         
     | 
| 
         | 
|
| 42 | 
         | 
| 43 | 
         | 
| 44 | 
         
             
            @output_on_top
         
     | 
| 45 | 
         
            +
            @ops.input_position(llm="bottom")
         
     | 
| 46 | 
         
             
            @op("Text embedder")
         
     | 
| 47 | 
         
             
            def text_embedder(llm, *, model="text-embedding-ada-002"):
         
     | 
| 48 | 
         
             
                llm = llm[0]["llm"]
         
     | 
| 
         | 
|
| 51 | 
         | 
| 52 | 
         | 
| 53 | 
         
             
            @output_on_top
         
     | 
| 54 | 
         
            +
            @ops.input_position(vector_store="bottom", text_embedder="bottom")
         
     | 
| 55 | 
         
             
            @op("RAG graph")
         
     | 
| 56 | 
         
             
            def rag_graph(vector_store, text_embedder):
         
     | 
| 57 | 
         
             
                vector_store = vector_store[0]["vector_store"]
         
     | 
| 
         | 
|
| 78 | 
         | 
| 79 | 
         | 
| 80 | 
         
             
            @output_on_top
         
     | 
| 81 | 
         
            +
            @ops.input_position(rag_graph="bottom", scenario_selector="bottom", llm="bottom")
         
     | 
| 82 | 
         
             
            @op("RAG chatbot")
         
     | 
| 83 | 
         
             
            def rag_chatbot(
         
     | 
| 84 | 
         
             
                rag_graph,
         
     | 
| 
         | 
|
| 107 | 
         | 
| 108 | 
         | 
| 109 | 
         
             
            @output_on_top
         
     | 
| 110 | 
         
            +
            @ops.input_position(processor="bottom")
         
     | 
| 111 | 
         
             
            @op("Chat processor")
         
     | 
| 112 | 
         
             
            def chat_processor(processor, *, _ctx: one_by_one.Context):
         
     | 
| 113 | 
         
             
                cfg = _ctx.last_result or {
         
     | 
| 
         | 
|
| 152 | 
         
             
                }
         
     | 
| 153 | 
         | 
| 154 | 
         | 
| 155 | 
         
            +
            @ops.input_position(chat_api="bottom")
         
     | 
| 156 | 
         
             
            @op("Test Chat API")
         
     | 
| 157 | 
         
             
            async def test_chat_api(message, chat_api, *, show_details=False):
         
     | 
| 158 | 
         
             
                chat_api = chat_api[0]["chat_api"]
         
     | 
| 
         | 
|
| 173 | 
         | 
| 174 | 
         | 
| 175 | 
         
             
            @output_on_top
         
     | 
| 176 | 
         
            +
            @ops.input_position(chatbot="bottom", chat_processor="bottom", knowledge_base="bottom")
         
     | 
| 177 | 
         
             
            @op("Chat API")
         
     | 
| 178 | 
         
             
            def chat_api(chatbot, chat_processor, knowledge_base, *, model="gpt-4o-mini"):
         
     | 
| 179 | 
         
             
                chatbot = chatbot[0]["chatbot"]
         
     | 
| 
         | 
|
| 205 | 
         
             
                }
         
     | 
| 206 | 
         | 
| 207 | 
         | 
| 208 | 
         
            +
            @op("View", view="table_view")
         
     | 
| 209 | 
         
             
            def view(input):
         
     | 
| 210 | 
         
             
                columns = [str(c) for c in input.keys() if not str(c).startswith("_")]
         
     | 
| 211 | 
         
             
                v = {
         
     | 
    	
        lynxkite-lynxscribe/tests/test_llm_ops.py
    CHANGED
    
    | 
         @@ -8,7 +8,7 @@ def make_node(id, op, type="basic", **params): 
     | 
|
| 8 | 
         
             
                return workspace.WorkspaceNode(
         
     | 
| 9 | 
         
             
                    id=id,
         
     | 
| 10 | 
         
             
                    type=type,
         
     | 
| 11 | 
         
            -
                    position=workspace. 
     | 
| 12 | 
         
             
                    data=workspace.WorkspaceNodeData(title=op, params=params),
         
     | 
| 13 | 
         
             
                )
         
     | 
| 14 | 
         | 
| 
         @@ -43,7 +43,7 @@ class LLMOpsTest(unittest.IsolatedAsyncioTestCase): 
     | 
|
| 43 | 
         
             
                                filename="/Users/danieldarabos/Downloads/aimo-train.csv",
         
     | 
| 44 | 
         
             
                                key="problem",
         
     | 
| 45 | 
         
             
                            ),
         
     | 
| 46 | 
         
            -
                            make_node("1", "View", type= 
     | 
| 47 | 
         
             
                        ],
         
     | 
| 48 | 
         
             
                        edges=[make_edge("0", "1")],
         
     | 
| 49 | 
         
             
                    )
         
     | 
| 
         | 
|
| 8 | 
         
             
                return workspace.WorkspaceNode(
         
     | 
| 9 | 
         
             
                    id=id,
         
     | 
| 10 | 
         
             
                    type=type,
         
     | 
| 11 | 
         
            +
                    position=workspace.Position(x=0, y=0),
         
     | 
| 12 | 
         
             
                    data=workspace.WorkspaceNodeData(title=op, params=params),
         
     | 
| 13 | 
         
             
                )
         
     | 
| 14 | 
         | 
| 
         | 
|
| 43 | 
         
             
                                filename="/Users/danieldarabos/Downloads/aimo-train.csv",
         
     | 
| 44 | 
         
             
                                key="problem",
         
     | 
| 45 | 
         
             
                            ),
         
     | 
| 46 | 
         
            +
                            make_node("1", "View", type="table_view"),
         
     | 
| 47 | 
         
             
                        ],
         
     | 
| 48 | 
         
             
                        edges=[make_edge("0", "1")],
         
     | 
| 49 | 
         
             
                    )
         
     | 
    	
        lynxkite-pillow-example/src/lynxkite_plugins/pillow_example/__init__.py
    CHANGED
    
    | 
         @@ -56,7 +56,7 @@ def to_grayscale(image: Image): 
     | 
|
| 56 | 
         
             
                return image.convert("L")
         
     | 
| 57 | 
         | 
| 58 | 
         | 
| 59 | 
         
            -
            @op("View image", view= 
     | 
| 60 | 
         
             
            def view_image(image: Image):
         
     | 
| 61 | 
         
             
                buffered = io.BytesIO()
         
     | 
| 62 | 
         
             
                image.save(buffered, format="JPEG")
         
     | 
| 
         | 
|
| 56 | 
         
             
                return image.convert("L")
         
     | 
| 57 | 
         | 
| 58 | 
         | 
| 59 | 
         
            +
            @op("View image", view="image")
         
     | 
| 60 | 
         
             
            def view_image(image: Image):
         
     | 
| 61 | 
         
             
                buffered = io.BytesIO()
         
     | 
| 62 | 
         
             
                image.save(buffered, format="JPEG")
         
     |