Source code for langchain_core.runnables.graph_ascii
"""Draws DAG in ASCII.Adapted from https://github.com/iterative/dvc/blob/main/dvc/dagascii.py."""importmathimportosfromcollections.abcimportMapping,SequencefromtypingimportAnyfromlangchain_core.runnables.graphimportEdgeasLangEdge
[docs]classVertexViewer:"""Class to define vertex box boundaries that will be accounted for during graph building by grandalf. Args: name (str): name of the vertex. """HEIGHT=3# top and bottom box edges + text"""Height of the box."""
[docs]def__init__(self,name:str)->None:self._h=self.HEIGHT# top and bottom box edges + textself._w=len(name)+2# right and left bottom edges + text
@propertydefh(self)->int:"""Height of the box."""returnself._h@propertydefw(self)->int:"""Width of the box."""returnself._w
[docs]classAsciiCanvas:"""Class for drawing in ASCII. Args: cols (int): number of columns in the canvas. Should be > 1. lines (int): number of lines in the canvas. Should be > 1. """TIMEOUT=10
[docs]def__init__(self,cols:int,lines:int)->None:ifcols<=1orlines<=1:msg="Canvas dimensions should be > 1"raiseValueError(msg)self.cols=colsself.lines=linesself.canvas=[[" "]*colsforlineinrange(lines)]
[docs]defdraw(self)->str:"""Draws ASCII canvas on the screen."""lines=map("".join,self.canvas)returnos.linesep.join(lines)
[docs]defpoint(self,x:int,y:int,char:str)->None:"""Create a point on ASCII canvas. Args: x (int): x coordinate. Should be >= 0 and < number of columns in the canvas. y (int): y coordinate. Should be >= 0 an < number of lines in the canvas. char (str): character to place in the specified point on the canvas. """iflen(char)!=1:msg="char should be a single character"raiseValueError(msg)ifx>=self.colsorx<0:msg="x should be >= 0 and < number of columns"raiseValueError(msg)ify>=self.linesory<0:msg="y should be >= 0 and < number of lines"raiseValueError(msg)self.canvas[y][x]=char
[docs]defline(self,x0:int,y0:int,x1:int,y1:int,char:str)->None:"""Create a line on ASCII canvas. Args: x0 (int): x coordinate where the line should start. y0 (int): y coordinate where the line should start. x1 (int): x coordinate where the line should end. y1 (int): y coordinate where the line should end. char (str): character to draw the line with. """ifx0>x1:x1,x0=x0,x1y1,y0=y0,y1dx=x1-x0dy=y1-y0ifdx==0anddy==0:self.point(x0,y0,char)elifabs(dx)>=abs(dy):forxinrange(x0,x1+1):y=y0ifdx==0elsey0+int(round((x-x0)*dy/float(dx)))self.point(x,y,char)elify0<y1:foryinrange(y0,y1+1):x=x0ifdy==0elsex0+int(round((y-y0)*dx/float(dy)))self.point(x,y,char)else:foryinrange(y1,y0+1):x=x0ifdy==0elsex1+int(round((y-y1)*dx/float(dy)))self.point(x,y,char)
[docs]deftext(self,x:int,y:int,text:str)->None:"""Print a text on ASCII canvas. Args: x (int): x coordinate where the text should start. y (int): y coordinate where the text should start. text (str): string that should be printed. """fori,charinenumerate(text):self.point(x+i,y,char)
[docs]defbox(self,x0:int,y0:int,width:int,height:int)->None:"""Create a box on ASCII canvas. Args: x0 (int): x coordinate of the box corner. y0 (int): y coordinate of the box corner. width (int): box width. height (int): box height. """ifwidth<=1orheight<=1:msg="Box dimensions should be > 1"raiseValueError(msg)width-=1height-=1forxinrange(x0,x0+width):self.point(x,y0,"-")self.point(x,y0+height,"-")foryinrange(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(vertices:Mapping[str,str],edges:Sequence[LangEdge])->Any:try:fromgrandalf.graphsimportEdge,Graph,Vertex# type: ignore[import]fromgrandalf.layoutsimportSugiyamaLayout# type: ignore[import]fromgrandalf.routingimport(# type: ignore[import]EdgeViewer,route_with_lines,)exceptImportErrorasexc:msg="Install grandalf to draw graphs: `pip install grandalf`."raiseImportError(msg)fromexc## Just a reminder about naming conventions:# +------------X# |# |# |# |# Y#vertices_={id:Vertex(f" {data} ")forid,datainvertices.items()}edges_=[Edge(vertices_[s],vertices_[e],data=cond)fors,e,_,condinedges]vertices_list=vertices_.values()graph=Graph(vertices_list,edges_)forvertexinvertices_list:vertex.view=VertexViewer(vertex.data)# NOTE: determine min box length to create the best layoutminw=min(v.view.wforvinvertices_list)foredgeinedges_:edge.view=EdgeViewer()sug=SugiyamaLayout(graph.C[0])graph=graph.C[0]roots=list(filter(lambdax:len(x.e_in())==0,graph.sV))sug.init_all(roots=roots,optimize=True)sug.yspace=VertexViewer.HEIGHTsug.xspace=minwsug.route_edge=route_with_linessug.draw()returnsug
[docs]defdraw_ascii(vertices:Mapping[str,str],edges:Sequence[LangEdge])->str:"""Build a DAG and draw it in ASCII. Args: vertices (list): list of graph vertices. edges (list): list of graph edges. Returns: str: ASCII representation Example: .. code-block:: python from langchain_core.runnables.graph_ascii import draw_ascii vertices = {1: "1", 2: "2", 3: "3", 4: "4"} edges = [ (source, target, None, None) for source, target in [(1, 2), (2, 3), (2, 4), (1, 4)] ] print(draw_ascii(vertices, edges)) .. code-block:: none +---+ | 1 | +---+ * * * * * * +---+ * | 2 | * +---+** * * ** * * ** * * ** +---+ +---+ | 3 | | 4 | +---+ +---+ """# NOTE: coordinates might me negative, so we need to shift# everything to the positive plane before we actually draw it.xlist:list[float]=[]ylist:list[float]=[]sug=_build_sugiyama_layout(vertices,edges)forvertexinsug.g.sV:# NOTE: moving boxes w/2 to the leftxlist.extend((vertex.view.xy[0]-vertex.view.w/2.0,vertex.view.xy[0]+vertex.view.w/2.0,))ylist.extend((vertex.view.xy[1],vertex.view.xy[1]+vertex.view.h))foredgeinsug.g.sE:forx,yinedge.view._pts:xlist.append(x)ylist.append(y)minx=min(xlist)miny=min(ylist)maxx=max(xlist)maxy=max(ylist)canvas_cols=int(math.ceil(math.ceil(maxx)-math.floor(minx)))+1canvas_lines=int(round(maxy-miny))canvas=AsciiCanvas(canvas_cols,canvas_lines)# NOTE: first draw edges so that node boxes could overwrite themforedgeinsug.g.sE:iflen(edge.view._pts)<=1:msg="Not enough points to draw an edge"raiseValueError(msg)forindexinrange(1,len(edge.view._pts)):start=edge.view._pts[index-1]end=edge.view._pts[index]start_x=int(round(start[0]-minx))start_y=int(round(start[1]-miny))end_x=int(round(end[0]-minx))end_y=int(round(end[1]-miny))ifstart_x<0orstart_y<0orend_x<0orend_y<0:msg=("Invalid edge coordinates: "f"start_x={start_x}, "f"start_y={start_y}, "f"end_x={end_x}, "f"end_y={end_y}")raiseValueError(msg)canvas.line(start_x,start_y,end_x,end_y,"."ifedge.dataelse"*")forvertexinsug.g.sV:# NOTE: moving boxes w/2 to the leftx=vertex.view.xy[0]-vertex.view.w/2.0y=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)returncanvas.draw()