276 lines
8.1 KiB
Go
276 lines
8.1 KiB
Go
package platform
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
type canvasAIReader struct {
|
|
Load func(string) (canvasSnapshot, error)
|
|
Parent func(string) (subgraphRelation, error)
|
|
snapshots map[string]canvasSnapshot
|
|
documents map[string]map[string]any
|
|
parents map[string]subgraphRelation
|
|
parentKnown map[string]bool
|
|
used map[string]map[string]bool
|
|
}
|
|
|
|
func (reader *canvasAIReader) snapshot(board string) (canvasSnapshot, map[string]any, error) {
|
|
if snapshot, ok := reader.snapshots[board]; ok {
|
|
return snapshot, reader.documents[board], nil
|
|
}
|
|
snapshot, err := reader.Load(board)
|
|
if err != nil {
|
|
return canvasSnapshot{}, nil, err
|
|
}
|
|
var doc map[string]any
|
|
if err = json.Unmarshal(snapshot.Document, &doc); err != nil {
|
|
return canvasSnapshot{}, nil, err
|
|
}
|
|
reader.snapshots[board] = snapshot
|
|
reader.documents[board] = doc
|
|
return snapshot, doc, nil
|
|
}
|
|
func (reader *canvasAIReader) parent(board string) (subgraphRelation, error) {
|
|
if reader.parentKnown[board] {
|
|
return reader.parents[board], nil
|
|
}
|
|
relation, err := reader.Parent(board)
|
|
if err == sql.ErrNoRows {
|
|
err = nil
|
|
relation = subgraphRelation{}
|
|
}
|
|
if err != nil {
|
|
return relation, err
|
|
}
|
|
reader.parentKnown[board] = true
|
|
reader.parents[board] = relation
|
|
return relation, nil
|
|
}
|
|
func (reader *canvasAIReader) node(board, node string) (map[string]any, error) {
|
|
_, doc, err := reader.snapshot(board)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, value := range doc["nodes"].([]any) {
|
|
n := value.(map[string]any)
|
|
if n["id"] == node {
|
|
return n["data"].(map[string]any), nil
|
|
}
|
|
}
|
|
return nil, errGraphOwnership
|
|
}
|
|
func (reader *canvasAIReader) resolve(board, node, handle, direction string, depth int) (string, error) {
|
|
if depth > 8 {
|
|
return "", errGraphCapacity
|
|
}
|
|
data, err := reader.node(board, node)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if data["kind"] != "frame" {
|
|
if !canvasChoice(data["kind"], "text", "script", "plain-text", "image", "plain-image") {
|
|
return "", errGraphOwnership
|
|
}
|
|
return canvasNodeKey(board, node), nil
|
|
}
|
|
sub, _ := data["subgraph"].(map[string]any)
|
|
child, _ := sub["boardId"].(string)
|
|
relation, err := reader.parent(child)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if relation.Parent != board || relation.Frame != node || relation.Archived {
|
|
return "", errGraphOwnership
|
|
}
|
|
ports, _ := data["ports"].([]any)
|
|
for _, value := range ports {
|
|
p := value.(map[string]any)
|
|
if p["direction"] == direction && "port:"+p["id"].(string) == handle {
|
|
return reader.resolve(child, p["nodeId"].(string), p["handle"].(string), direction, depth+1)
|
|
}
|
|
}
|
|
return "", errGraphOwnership
|
|
}
|
|
|
|
// Follow only real edges and the port chains exposing this node. Empty or
|
|
// unrelated child boards are never traversed merely because their frame exists.
|
|
func (reader *canvasAIReader) edges(board, node string, outgoing bool) ([]canvasAIEdgePlan, error) {
|
|
type exposure struct {
|
|
Board, Node, Handle string
|
|
Depth int
|
|
}
|
|
queue := []exposure{{Board: board, Node: node}}
|
|
seen := map[string]bool{}
|
|
found := map[string]canvasAIEdgePlan{}
|
|
end, direction := "target", "input"
|
|
if outgoing {
|
|
end, direction = "source", "output"
|
|
}
|
|
key := canvasNodeKey(board, node)
|
|
for len(queue) > 0 {
|
|
current := queue[0]
|
|
queue = queue[1:]
|
|
identity := current.Board + "/" + current.Node + "/" + current.Handle
|
|
if seen[identity] {
|
|
continue
|
|
}
|
|
seen[identity] = true
|
|
if current.Depth > 8 {
|
|
return nil, errGraphCapacity
|
|
}
|
|
_, doc, err := reader.snapshot(current.Board)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, value := range doc["edges"].([]any) {
|
|
edge := value.(map[string]any)
|
|
if edge[end] != current.Node {
|
|
continue
|
|
}
|
|
handle, _ := edge[end+"Handle"].(string)
|
|
if current.Handle != "" && handle != current.Handle {
|
|
continue
|
|
}
|
|
data, _ := edge["data"].(map[string]any)
|
|
execution, _ := data["execution"].(map[string]any)
|
|
if execution["enabled"] == false {
|
|
continue
|
|
}
|
|
sh, _ := edge["sourceHandle"].(string)
|
|
th, _ := edge["targetHandle"].(string)
|
|
source, err := reader.resolve(current.Board, edge["source"].(string), sh, "output", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
target, err := reader.resolve(current.Board, edge["target"].(string), th, "input", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if (outgoing && source != key) || (!outgoing && target != key) {
|
|
continue
|
|
}
|
|
id := edge["id"].(string)
|
|
if reader.used[current.Board] == nil {
|
|
reader.used[current.Board] = map[string]bool{}
|
|
}
|
|
reader.used[current.Board][id] = true
|
|
logical := canvasAIEdgePlan{ID: canvasNodeKey(current.Board, id), Source: source, Target: target}
|
|
found[logical.ID] = logical
|
|
}
|
|
relation, err := reader.parent(current.Board)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if relation.Parent == "" {
|
|
continue
|
|
}
|
|
if relation.Archived {
|
|
return nil, errGraphOwnership
|
|
}
|
|
frame, err := reader.node(relation.Parent, relation.Frame)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sub, _ := frame["subgraph"].(map[string]any)
|
|
if sub["boardId"] != current.Board {
|
|
return nil, errGraphOwnership
|
|
}
|
|
ports, _ := frame["ports"].([]any)
|
|
for _, value := range ports {
|
|
p := value.(map[string]any)
|
|
if p["nodeId"] == current.Node && p["direction"] == direction && (current.Handle == "" || p["handle"] == current.Handle) {
|
|
queue = append(queue, exposure{Board: relation.Parent, Node: relation.Frame, Handle: "port:" + p["id"].(string), Depth: current.Depth + 1})
|
|
}
|
|
}
|
|
}
|
|
result := []canvasAIEdgePlan{}
|
|
for _, edge := range found {
|
|
result = append(result, edge)
|
|
}
|
|
sort.Slice(result, func(i, j int) bool { return result[i].ID < result[j].ID })
|
|
return result, nil
|
|
}
|
|
func (reader *canvasAIReader) plan(board, node string, propagate bool) (canvasAIPlan, error) {
|
|
reader.snapshots = map[string]canvasSnapshot{}
|
|
reader.documents = map[string]map[string]any{}
|
|
reader.parents = map[string]subgraphRelation{}
|
|
reader.parentKnown = map[string]bool{}
|
|
reader.used = map[string]map[string]bool{}
|
|
queue := []string{canvasNodeKey(board, node)}
|
|
selected := map[string]bool{}
|
|
for len(queue) > 0 {
|
|
key := queue[0]
|
|
queue = queue[1:]
|
|
if selected[key] {
|
|
continue
|
|
}
|
|
selected[key] = true
|
|
if len(selected) > 64 {
|
|
return canvasAIPlan{}, errGraphCapacity
|
|
}
|
|
parts := strings.SplitN(key, "/", 2)
|
|
data, err := reader.node(parts[0], parts[1])
|
|
if err != nil {
|
|
return canvasAIPlan{}, err
|
|
}
|
|
ai, _ := data["ai"].(map[string]any)
|
|
model, _ := ai["modelId"].(string)
|
|
if key != canvasNodeKey(board, node) && (ai["enabled"] != true || model == "") {
|
|
continue
|
|
}
|
|
if _, err = reader.edges(parts[0], parts[1], false); err != nil {
|
|
return canvasAIPlan{}, err
|
|
}
|
|
if propagate {
|
|
edges, err := reader.edges(parts[0], parts[1], true)
|
|
if err != nil {
|
|
return canvasAIPlan{}, err
|
|
}
|
|
for _, edge := range edges {
|
|
queue = append(queue, edge.Target)
|
|
}
|
|
}
|
|
}
|
|
filtered := map[string]canvasSnapshot{}
|
|
for id, snapshot := range reader.snapshots {
|
|
doc := reader.documents[id]
|
|
copy := map[string]any{}
|
|
for key, value := range doc {
|
|
copy[key] = value
|
|
}
|
|
edges := []any{}
|
|
for _, value := range doc["edges"].([]any) {
|
|
edge := value.(map[string]any)
|
|
if reader.used[id][edge["id"].(string)] {
|
|
edges = append(edges, value)
|
|
}
|
|
}
|
|
copy["edges"] = edges
|
|
snapshot.Document, _ = json.Marshal(copy)
|
|
filtered[id] = snapshot
|
|
}
|
|
return buildCanvasAIPlan(filtered, board, node, propagate)
|
|
}
|
|
func loadCanvasAIPlan(r *http.Request, tx *sql.Tx, project, workspace, account string, parent canvasSnapshot, node string, propagate bool) (canvasAIPlan, error) {
|
|
reader := canvasAIReader{Load: func(board string) (canvasSnapshot, error) {
|
|
_, role, err := lockMaterial(r.Context(), tx, project, workspace, board, account)
|
|
if err != nil {
|
|
return canvasSnapshot{}, err
|
|
}
|
|
if role == "viewer" {
|
|
return canvasSnapshot{}, sql.ErrNoRows
|
|
}
|
|
if board == parent.BoardID {
|
|
return parent, nil
|
|
}
|
|
return loadCanvas(r, tx, project, workspace, board)
|
|
}, Parent: func(board string) (subgraphRelation, error) {
|
|
return readSubgraphRelation(r, tx, project, workspace, board)
|
|
}}
|
|
return reader.plan(parent.BoardID, node, propagate)
|
|
}
|