refactor: update graph visualization and add ASCII representation (#3219)
* chore: Update dependencies in poetry files, adding grandalf 0.8.0 and upgrading several other packages to latest versions * refactor: Add ASCII graph drawing functionality using grandalf library * refactor: Improve Graph representation format and add ASCII visualization method to enhance readability and usability
This commit is contained in:
parent
56523a7f2d
commit
72463d9b5d
5 changed files with 851 additions and 545 deletions
18
poetry.lock
generated
18
poetry.lock
generated
|
|
@ -3404,6 +3404,23 @@ test = ["aiofiles", "aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)", "botocore
|
|||
test-no-transport = ["aiofiles", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "vcrpy (==4.4.0)"]
|
||||
websockets = ["websockets (>=10,<12)"]
|
||||
|
||||
[[package]]
|
||||
name = "grandalf"
|
||||
version = "0.8"
|
||||
description = "Graph and drawing algorithms framework"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "grandalf-0.8-py3-none-any.whl", hash = "sha256:793ca254442f4a79252ea9ff1ab998e852c1e071b863593e5383afee906b4185"},
|
||||
{file = "grandalf-0.8.tar.gz", hash = "sha256:2813f7aab87f0d20f334a3162ccfbcbf085977134a17a5b516940a93a77ea974"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pyparsing = "*"
|
||||
|
||||
[package.extras]
|
||||
full = ["numpy", "ply"]
|
||||
|
||||
[[package]]
|
||||
name = "graphql-core"
|
||||
version = "3.2.3"
|
||||
|
|
@ -5171,6 +5188,7 @@ emoji = "^2.12.0"
|
|||
fastapi = "^0.111.0"
|
||||
filelock = "^3.15.4"
|
||||
firecrawl-py = "^0.0.16"
|
||||
grandalf = "^0.8.0"
|
||||
gunicorn = "^22.0.0"
|
||||
httpx = "*"
|
||||
jq = {version = "^1.7.0", markers = "sys_platform != \"win32\""}
|
||||
|
|
|
|||
185
src/backend/base/langflow/graph/graph/ascii.py
Normal file
185
src/backend/base/langflow/graph/graph/ascii.py
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
"""
|
||||
This code is adapted from the DVC project.
|
||||
|
||||
Original source:
|
||||
https://github.com/iterative/dvc/blob/c5bac1c8cfdb2c0f54d52ac61ff754e6f583822a/dvc/dagascii.py
|
||||
|
||||
The original code has been modified to focus on drawing a Directed Acyclic Graph (DAG) in ASCII
|
||||
using the `grandalf` library. Non-essential parts have been removed, and the code has been
|
||||
refactored to suit this specific use case.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
from grandalf.graphs import Edge as GrandalfEdge
|
||||
from grandalf.graphs import Graph as GrandalfGraph
|
||||
from grandalf.graphs import Vertex as GrandalfVertex
|
||||
from grandalf.layouts import SugiyamaLayout
|
||||
from grandalf.routing import EdgeViewer, route_with_lines
|
||||
|
||||
|
||||
class VertexViewer:
|
||||
"""Class to define vertex box boundaries that will be accounted for during graph building by grandalf."""
|
||||
|
||||
HEIGHT = 3 # top and bottom box edges + text
|
||||
|
||||
def __init__(self, name):
|
||||
self._h = self.HEIGHT # top and bottom box edges + text
|
||||
self._w = len(name) + 2 # right and left bottom edges + text
|
||||
|
||||
@property
|
||||
def h(self):
|
||||
return self._h
|
||||
|
||||
@property
|
||||
def w(self):
|
||||
return self._w
|
||||
|
||||
|
||||
class AsciiCanvas:
|
||||
"""Class for drawing in ASCII."""
|
||||
|
||||
def __init__(self, cols, lines):
|
||||
assert cols > 1
|
||||
assert lines > 1
|
||||
self.cols = cols
|
||||
self.lines = lines
|
||||
self.canvas = [[" "] * cols for _ in range(lines)]
|
||||
|
||||
def get_lines(self):
|
||||
return map("".join, self.canvas)
|
||||
|
||||
def draws(self):
|
||||
return "\n".join(self.get_lines())
|
||||
|
||||
def draw(self):
|
||||
"""Draws ASCII canvas on the screen."""
|
||||
lines = self.get_lines()
|
||||
print("\n".join(lines))
|
||||
|
||||
def point(self, x, y, char):
|
||||
"""Create a point on ASCII canvas."""
|
||||
assert len(char) == 1
|
||||
assert 0 <= x < self.cols
|
||||
assert 0 <= y < self.lines
|
||||
self.canvas[y][x] = char
|
||||
|
||||
def line(self, x0, y0, x1, y1, char):
|
||||
"""Create a line on ASCII canvas."""
|
||||
if x0 > x1:
|
||||
x1, x0 = x0, x1
|
||||
y1, y0 = y0, y1
|
||||
|
||||
dx = x1 - x0
|
||||
dy = y1 - y0
|
||||
|
||||
if dx == 0 and dy == 0:
|
||||
self.point(x0, y0, char)
|
||||
elif abs(dx) >= abs(dy):
|
||||
for x in range(x0, x1 + 1):
|
||||
y = y0 + int(round((x - x0) * dy / float(dx))) if dx else y0
|
||||
self.point(x, y, char)
|
||||
else:
|
||||
for y in range(min(y0, y1), max(y0, y1) + 1):
|
||||
x = x0 + int(round((y - y0) * dx / float(dy))) if dy else x0
|
||||
self.point(x, y, char)
|
||||
|
||||
def text(self, x, y, text):
|
||||
"""Print a text on ASCII canvas."""
|
||||
for i, char in enumerate(text):
|
||||
self.point(x + i, y, char)
|
||||
|
||||
def box(self, x0, y0, width, height):
|
||||
"""Create a box on ASCII canvas."""
|
||||
assert width > 1
|
||||
assert height > 1
|
||||
width -= 1
|
||||
height -= 1
|
||||
|
||||
for x in range(x0, x0 + width):
|
||||
self.point(x, y0, "-")
|
||||
self.point(x, y0 + height, "-")
|
||||
for y in range(y0, y0 + height):
|
||||
self.point(x0, y, "|")
|
||||
self.point(x0 + width, y, "|")
|
||||
self.point(x0, y0, "+")
|
||||
self.point(x0 + width, y0, "+")
|
||||
self.point(x0, y0 + height, "+")
|
||||
self.point(x0 + width, y0 + height, "+")
|
||||
|
||||
|
||||
def build_sugiyama_layout(vertexes, edges):
|
||||
vertexes = {v: GrandalfVertex(v) for v in vertexes}
|
||||
edges = [GrandalfEdge(vertexes[s], vertexes[e]) for s, e in edges]
|
||||
graph = GrandalfGraph(vertexes.values(), edges)
|
||||
|
||||
for vertex in vertexes.values():
|
||||
vertex.view = VertexViewer(vertex.data)
|
||||
|
||||
minw = min([v.view.w for v in vertexes.values()])
|
||||
|
||||
for edge in edges:
|
||||
edge.view = EdgeViewer()
|
||||
|
||||
sug = SugiyamaLayout(graph.C[0])
|
||||
roots = [v for v in sug.g.sV if len(v.e_in()) == 0]
|
||||
sug.init_all(roots=roots, optimize=True)
|
||||
|
||||
sug.yspace = VertexViewer.HEIGHT
|
||||
sug.xspace = minw
|
||||
sug.route_edge = route_with_lines
|
||||
|
||||
sug.draw()
|
||||
return sug
|
||||
|
||||
|
||||
def draw_graph(vertexes, edges, return_ascii=True):
|
||||
"""Build a DAG and draw it in ASCII."""
|
||||
sug = build_sugiyama_layout(vertexes, edges)
|
||||
|
||||
Xs = []
|
||||
Ys = []
|
||||
|
||||
for vertex in sug.g.sV:
|
||||
Xs.extend([vertex.view.xy[0] - vertex.view.w / 2.0, vertex.view.xy[0] + vertex.view.w / 2.0])
|
||||
Ys.extend([vertex.view.xy[1], vertex.view.xy[1] + vertex.view.h])
|
||||
|
||||
for edge in sug.g.sE:
|
||||
for x, y in edge.view._pts:
|
||||
Xs.append(x)
|
||||
Ys.append(y)
|
||||
|
||||
minx = min(Xs)
|
||||
miny = min(Ys)
|
||||
maxx = max(Xs)
|
||||
maxy = max(Ys)
|
||||
|
||||
canvas_cols = int(math.ceil(maxx - minx)) + 1
|
||||
canvas_lines = int(round(maxy - miny))
|
||||
|
||||
canvas = AsciiCanvas(canvas_cols, canvas_lines)
|
||||
|
||||
for edge in sug.g.sE:
|
||||
assert len(edge.view._pts) > 1
|
||||
for index in range(1, len(edge.view._pts)):
|
||||
start = edge.view._pts[index - 1]
|
||||
end = edge.view._pts[index]
|
||||
canvas.line(
|
||||
int(round(start[0] - minx)),
|
||||
int(round(start[1] - miny)),
|
||||
int(round(end[0] - minx)),
|
||||
int(round(end[1] - miny)),
|
||||
"*",
|
||||
)
|
||||
|
||||
for vertex in sug.g.sV:
|
||||
x = vertex.view.xy[0] - vertex.view.w / 2.0
|
||||
y = vertex.view.xy[1]
|
||||
canvas.box(int(round(x - minx)), int(round(y - miny)), vertex.view.w, vertex.view.h)
|
||||
canvas.text(int(round(x - minx)) + 1, int(round(y - miny)) + 1, vertex.data)
|
||||
if return_ascii:
|
||||
return canvas.draws()
|
||||
else:
|
||||
canvas.draw()
|
||||
|
|
@ -1516,8 +1516,16 @@ class Graph:
|
|||
|
||||
def __repr__(self):
|
||||
vertex_ids = [vertex.id for vertex in self.vertices]
|
||||
edges_repr = "\n".join([f"{edge.source_id} --> {edge.target_id}" for edge in self.edges])
|
||||
return f"Graph: \nNodes: {vertex_ids}\nConnections: \n{edges_repr}"
|
||||
edges_repr = "\n".join([f" {edge.source_id} --> {edge.target_id}" for edge in self.edges])
|
||||
|
||||
return (
|
||||
f"Graph Representation:\n"
|
||||
f"----------------------\n"
|
||||
f"Vertices ({len(vertex_ids)}):\n"
|
||||
f" {', '.join(map(str, vertex_ids))}\n\n"
|
||||
f"Edges ({len(self.edges)}):\n"
|
||||
f"{edges_repr}"
|
||||
)
|
||||
|
||||
def layered_topological_sort(
|
||||
self,
|
||||
|
|
|
|||
1179
src/backend/base/poetry.lock
generated
1179
src/backend/base/poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -76,7 +76,7 @@ aiofiles = "^24.1.0"
|
|||
setuptools = ">=70"
|
||||
nanoid = "^2.0.0"
|
||||
filelock = "^3.15.4"
|
||||
|
||||
grandalf = "^0.8.0"
|
||||
[tool.poetry.extras]
|
||||
deploy = ["celery", "redis", "flower"]
|
||||
local = ["llama-cpp-python", "sentence-transformers", "ctransformers"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue