mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-08 04:22:20 +08:00
### What problem does this PR solve? go-version of Pregel-based BSP engine ### Type of change - [x] New Feature (non-breaking change which adds functionality)
395 lines
11 KiB
Go
395 lines
11 KiB
Go
// Package visualization provides graph visualization utilities for Agent Harness Go.
|
|
// It supports multiple output formats including Mermaid, Graphviz DOT, and ASCII art.
|
|
package visualization
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"ragflow/internal/harness/graph/constants"
|
|
)
|
|
|
|
// DrawFormat represents the output format for graph visualization.
|
|
type DrawFormat string
|
|
|
|
const (
|
|
// FormatASCII generates ASCII art representation.
|
|
FormatASCII DrawFormat = "ascii"
|
|
// FormatMermaid generates Mermaid flowchart syntax.
|
|
FormatMermaid DrawFormat = "mermaid"
|
|
// FormatGraphviz generates Graphviz DOT format.
|
|
FormatGraphviz DrawFormat = "graphviz"
|
|
)
|
|
|
|
// DrawOptions configures the graph drawing behavior.
|
|
type DrawOptions struct {
|
|
// Format specifies the output format.
|
|
Format DrawFormat
|
|
// Horizontal determines if the graph should be drawn horizontally (left to right).
|
|
Horizontal bool
|
|
// ShowStartEnd determines if START and END nodes should be shown.
|
|
ShowStartEnd bool
|
|
// NodeStyles allows custom styling for specific nodes.
|
|
NodeStyles map[string]string
|
|
// EdgeStyles allows custom styling for specific edges.
|
|
EdgeStyles map[string]string
|
|
}
|
|
|
|
// DefaultDrawOptions returns default drawing options.
|
|
func DefaultDrawOptions() *DrawOptions {
|
|
return &DrawOptions{
|
|
Format: FormatMermaid,
|
|
Horizontal: true,
|
|
ShowStartEnd: true,
|
|
NodeStyles: make(map[string]string),
|
|
EdgeStyles: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
// GraphProvider is the interface required to draw a graph.
|
|
// Implement this interface for your graph type to enable visualization.
|
|
type GraphProvider interface {
|
|
// GetNodes returns all node names in the graph.
|
|
GetNodes() []string
|
|
// GetEntryPoint returns the entry point node name.
|
|
GetEntryPoint() string
|
|
// GetEdges returns all edges as (from, to) pairs.
|
|
GetEdges() [][2]string
|
|
// GetConditionalEdges returns conditional edges as (from, condition, to) triples.
|
|
GetConditionalEdges() [][3]string
|
|
}
|
|
|
|
// DrawGraph generates a visual representation of the graph.
|
|
func DrawGraph(graph GraphProvider, opts *DrawOptions) (string, error) {
|
|
if graph == nil {
|
|
return "", fmt.Errorf("graph cannot be nil")
|
|
}
|
|
|
|
if opts == nil {
|
|
opts = DefaultDrawOptions()
|
|
}
|
|
|
|
switch opts.Format {
|
|
case FormatASCII:
|
|
return drawASCII(graph, opts)
|
|
case FormatMermaid:
|
|
return drawMermaid(graph, opts)
|
|
case FormatGraphviz:
|
|
return drawGraphviz(graph, opts)
|
|
default:
|
|
return "", fmt.Errorf("unsupported format: %s", opts.Format)
|
|
}
|
|
}
|
|
|
|
// drawMermaid generates a Mermaid flowchart.
|
|
func drawMermaid(graph GraphProvider, opts *DrawOptions) (string, error) {
|
|
var sb strings.Builder
|
|
|
|
// Start the diagram
|
|
if opts.Horizontal {
|
|
sb.WriteString("graph LR\n")
|
|
} else {
|
|
sb.WriteString("graph TD\n")
|
|
}
|
|
|
|
// Track nodes for styling
|
|
nodes := make(map[string]bool)
|
|
startNode := graph.GetEntryPoint()
|
|
|
|
// Add regular edges
|
|
edges := graph.GetEdges()
|
|
for _, edge := range edges {
|
|
from, to := edge[0], edge[1]
|
|
nodes[from] = true
|
|
nodes[to] = true
|
|
|
|
fromID := sanitizeNodeID(from)
|
|
toID := sanitizeNodeID(to)
|
|
|
|
// Style START and END nodes
|
|
if from == constants.Start && opts.ShowStartEnd {
|
|
fromID = "START"
|
|
sb.WriteString(fmt.Sprintf(" %s((\"\"))\n", fromID))
|
|
}
|
|
if to == constants.End && opts.ShowStartEnd {
|
|
toID = "END"
|
|
sb.WriteString(fmt.Sprintf(" %s(((\"\")))\n", toID))
|
|
}
|
|
|
|
edgeStyle := ""
|
|
if style, ok := opts.EdgeStyles[fmt.Sprintf("%s->%s", from, to)]; ok {
|
|
edgeStyle = fmt.Sprintf(" |%s|", style)
|
|
}
|
|
|
|
sb.WriteString(fmt.Sprintf(" %s -->%s %s\n", fromID, edgeStyle, toID))
|
|
}
|
|
|
|
// Add conditional edges
|
|
condEdges := graph.GetConditionalEdges()
|
|
for _, edge := range condEdges {
|
|
from, condition, to := edge[0], edge[1], edge[2]
|
|
nodes[from] = true
|
|
nodes[to] = true
|
|
|
|
fromID := sanitizeNodeID(from)
|
|
toID := sanitizeNodeID(to)
|
|
|
|
if to == constants.End && opts.ShowStartEnd {
|
|
toID = "END"
|
|
}
|
|
|
|
label := sanitizeLabel(condition)
|
|
sb.WriteString(fmt.Sprintf(" %s -->|\"%s\"| %s\n", fromID, label, toID))
|
|
}
|
|
|
|
// Add node styles
|
|
for node := range nodes {
|
|
if style, ok := opts.NodeStyles[node]; ok {
|
|
nodeID := sanitizeNodeID(node)
|
|
sb.WriteString(fmt.Sprintf(" style %s %s\n", nodeID, style))
|
|
}
|
|
}
|
|
|
|
// Highlight entry point
|
|
if startNode != "" {
|
|
nodeID := sanitizeNodeID(startNode)
|
|
sb.WriteString(fmt.Sprintf(" style %s fill:#e1f5e1,stroke:#333,stroke-width:2px\n", nodeID))
|
|
}
|
|
|
|
return sb.String(), nil
|
|
}
|
|
|
|
// drawGraphviz generates a Graphviz DOT format diagram.
|
|
func drawGraphviz(graph GraphProvider, opts *DrawOptions) (string, error) {
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString("digraph Graph {\n")
|
|
|
|
// Set direction
|
|
if opts.Horizontal {
|
|
sb.WriteString(" rankdir=LR;\n")
|
|
}
|
|
|
|
sb.WriteString(" node [shape=box, style=rounded];\n\n")
|
|
|
|
// Track nodes
|
|
nodes := make(map[string]bool)
|
|
startNode := graph.GetEntryPoint()
|
|
|
|
// Define special nodes
|
|
if opts.ShowStartEnd {
|
|
sb.WriteString(" // Special nodes\n")
|
|
sb.WriteString(fmt.Sprintf(" \"%s\" [shape=circle, label=\"\", width=0.5, style=filled, fillcolor=green];\n", constants.Start))
|
|
sb.WriteString(fmt.Sprintf(" \"%s\" [shape=doublecircle, label=\"\", width=0.5, style=filled, fillcolor=red];\n\n", constants.End))
|
|
}
|
|
|
|
// Define regular nodes
|
|
sb.WriteString(" // Nodes\n")
|
|
allNodes := graph.GetNodes()
|
|
sort.Strings(allNodes)
|
|
|
|
for _, node := range allNodes {
|
|
nodes[node] = true
|
|
attrs := []string{fmt.Sprintf("label=\"%s\"", node)}
|
|
|
|
// Highlight entry point
|
|
if node == startNode {
|
|
attrs = append(attrs, "style=filled", "fillcolor=lightblue")
|
|
} else if style, ok := opts.NodeStyles[node]; ok {
|
|
attrs = append(attrs, fmt.Sprintf("style=filled, fillcolor=%s", style))
|
|
}
|
|
|
|
sb.WriteString(fmt.Sprintf(" \"%s\" [%s];\n", node, strings.Join(attrs, ", ")))
|
|
}
|
|
|
|
// Add edges
|
|
sb.WriteString("\n // Edges\n")
|
|
|
|
// Regular edges
|
|
edges := graph.GetEdges()
|
|
for _, edge := range edges {
|
|
from, to := edge[0], edge[1]
|
|
attrs := ""
|
|
if style, ok := opts.EdgeStyles[fmt.Sprintf("%s->%s", from, to)]; ok {
|
|
attrs = fmt.Sprintf(" [%s]", style)
|
|
}
|
|
sb.WriteString(fmt.Sprintf(" \"%s\" -> \"%s\"%s;\n", from, to, attrs))
|
|
}
|
|
|
|
// Conditional edges
|
|
condEdges := graph.GetConditionalEdges()
|
|
for _, edge := range condEdges {
|
|
from, condition, to := edge[0], edge[1], edge[2]
|
|
label := sanitizeLabel(condition)
|
|
sb.WriteString(fmt.Sprintf(" \"%s\" -> \"%s\" [label=\"%s\"];\n", from, to, label))
|
|
}
|
|
|
|
sb.WriteString("}\n")
|
|
|
|
return sb.String(), nil
|
|
}
|
|
|
|
// drawASCII generates a simple ASCII art representation.
|
|
func drawASCII(graph GraphProvider, opts *DrawOptions) (string, error) {
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString("Graph Structure:\n")
|
|
sb.WriteString(strings.Repeat("=", 50))
|
|
sb.WriteString("\n\n")
|
|
|
|
// Entry point
|
|
startNode := graph.GetEntryPoint()
|
|
sb.WriteString(fmt.Sprintf("Entry Point: %s\n\n", startNode))
|
|
|
|
// Nodes
|
|
sb.WriteString("Nodes:\n")
|
|
nodes := graph.GetNodes()
|
|
sort.Strings(nodes)
|
|
for _, node := range nodes {
|
|
marker := " "
|
|
if node == startNode {
|
|
marker = "* "
|
|
}
|
|
sb.WriteString(fmt.Sprintf(" %s%s\n", marker, node))
|
|
}
|
|
|
|
// Edges
|
|
sb.WriteString("\nEdges:\n")
|
|
edges := graph.GetEdges()
|
|
for _, edge := range edges {
|
|
sb.WriteString(fmt.Sprintf(" %s --> %s\n", edge[0], edge[1]))
|
|
}
|
|
|
|
// Conditional edges
|
|
condEdges := graph.GetConditionalEdges()
|
|
if len(condEdges) > 0 {
|
|
sb.WriteString("\nConditional Edges:\n")
|
|
for _, edge := range condEdges {
|
|
sb.WriteString(fmt.Sprintf(" %s --[%s]--> %s\n", edge[0], edge[1], edge[2]))
|
|
}
|
|
}
|
|
|
|
return sb.String(), nil
|
|
}
|
|
|
|
// sanitizeNodeID creates a valid Mermaid/Graphviz node ID.
|
|
func sanitizeNodeID(name string) string {
|
|
// Replace special characters
|
|
id := strings.ReplaceAll(name, "-", "_")
|
|
id = strings.ReplaceAll(id, " ", "_")
|
|
id = strings.ReplaceAll(id, ".", "_")
|
|
|
|
// Ensure it starts with a letter or underscore
|
|
if len(id) > 0 && id[0] >= '0' && id[0] <= '9' {
|
|
id = "_" + id
|
|
}
|
|
|
|
return id
|
|
}
|
|
|
|
// sanitizeLabel creates a safe label string.
|
|
func sanitizeLabel(label string) string {
|
|
// Escape quotes
|
|
label = strings.ReplaceAll(label, "\"", "\\\"")
|
|
// Limit length
|
|
if len(label) > 50 {
|
|
label = label[:47] + "..."
|
|
}
|
|
return label
|
|
}
|
|
|
|
// DrawMermaid is a convenience function to draw a graph in Mermaid format.
|
|
func DrawMermaid(graph GraphProvider, horizontal bool) (string, error) {
|
|
opts := DefaultDrawOptions()
|
|
opts.Format = FormatMermaid
|
|
opts.Horizontal = horizontal
|
|
return DrawGraph(graph, opts)
|
|
}
|
|
|
|
// DrawGraphviz is a convenience function to draw a graph in Graphviz DOT format.
|
|
func DrawGraphviz(graph GraphProvider, horizontal bool) (string, error) {
|
|
opts := DefaultDrawOptions()
|
|
opts.Format = FormatGraphviz
|
|
opts.Horizontal = horizontal
|
|
return DrawGraph(graph, opts)
|
|
}
|
|
|
|
// DrawASCII is a convenience function to draw a graph in ASCII format.
|
|
func DrawASCII(graph GraphProvider) (string, error) {
|
|
opts := DefaultDrawOptions()
|
|
opts.Format = FormatASCII
|
|
return DrawGraph(graph, opts)
|
|
}
|
|
|
|
// SimpleGraph is a simple implementation of GraphProvider for testing and examples.
|
|
type SimpleGraph struct {
|
|
Nodes []string
|
|
EntryPointNode string
|
|
RegularEdges [][2]string
|
|
ConditionalEdges [][3]string
|
|
}
|
|
|
|
// GetNodes returns all nodes.
|
|
func (g *SimpleGraph) GetNodes() []string {
|
|
return g.Nodes
|
|
}
|
|
|
|
// GetEntryPoint returns the entry point.
|
|
func (g *SimpleGraph) GetEntryPoint() string {
|
|
return g.EntryPointNode
|
|
}
|
|
|
|
// GetEdges returns regular edges.
|
|
func (g *SimpleGraph) GetEdges() [][2]string {
|
|
return g.RegularEdges
|
|
}
|
|
|
|
// GetConditionalEdges returns conditional edges.
|
|
func (g *SimpleGraph) GetConditionalEdges() [][3]string {
|
|
return g.ConditionalEdges
|
|
}
|
|
|
|
// NewSimpleGraph creates a new simple graph for visualization.
|
|
func NewSimpleGraph(entryPoint string) *SimpleGraph {
|
|
return &SimpleGraph{
|
|
Nodes: []string{entryPoint},
|
|
EntryPointNode: entryPoint,
|
|
RegularEdges: make([][2]string, 0),
|
|
ConditionalEdges: make([][3]string, 0),
|
|
}
|
|
}
|
|
|
|
// AddNode adds a node to the graph.
|
|
func (g *SimpleGraph) AddNode(node string) {
|
|
for _, n := range g.Nodes {
|
|
if n == node {
|
|
return
|
|
}
|
|
}
|
|
g.Nodes = append(g.Nodes, node)
|
|
}
|
|
|
|
// AddEdge adds a regular edge.
|
|
func (g *SimpleGraph) AddEdge(from, to string) {
|
|
g.AddNode(from)
|
|
g.AddNode(to)
|
|
g.RegularEdges = append(g.RegularEdges, [2]string{from, to})
|
|
}
|
|
|
|
// AddConditionalEdge adds a conditional edge.
|
|
func (g *SimpleGraph) AddConditionalEdge(from, condition, to string) {
|
|
g.AddNode(from)
|
|
g.AddNode(to)
|
|
g.ConditionalEdges = append(g.ConditionalEdges, [3]string{from, condition, to})
|
|
}
|
|
|
|
// ExportToFile exports the graph visualization to a string that can be saved to a file.
|
|
// For Mermaid format, this can be used with Mermaid-compatible tools.
|
|
// For Graphviz, use the 'dot' command: dot -Tpng input.dot -o output.png
|
|
func ExportToFormat(graph GraphProvider, format DrawFormat) (string, error) {
|
|
opts := DefaultDrawOptions()
|
|
opts.Format = format
|
|
return DrawGraph(graph, opts)
|
|
}
|