225 lines
7.4 KiB
Go
225 lines
7.4 KiB
Go
package platform
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
type canvasAIStaticInput struct {
|
|
EdgeID string `json:"logical_edge_id"`
|
|
BoardID string `json:"board_id"`
|
|
NodeID string `json:"node_id"`
|
|
Kind string `json:"kind"`
|
|
Content map[string]any `json:"content"`
|
|
}
|
|
type canvasAIStepPlan struct {
|
|
CanonicalRoot string `json:"canonical_root,omitempty"`
|
|
TargetIncarnation int64 `json:"target_incarnation,omitempty"`
|
|
StaticInputs []canvasAIStaticInput `json:"static_inputs"`
|
|
MaxExecutionMS int `json:"max_execution_ms"`
|
|
ID string `json:"step_id"`
|
|
BoardID string `json:"board_id"`
|
|
NodeID string `json:"node_id"`
|
|
Kind string `json:"kind"`
|
|
ModelID string `json:"model_id"`
|
|
Config map[string]any `json:"config"`
|
|
FrozenContent map[string]any `json:"frozen_content"`
|
|
Fingerprint string `json:"target_fingerprint"`
|
|
Status string `json:"status"`
|
|
Dependencies []string `json:"dependencies"`
|
|
}
|
|
type canvasAIEdgePlan struct {
|
|
ID string `json:"logical_edge_id"`
|
|
Source string `json:"source_step_id"`
|
|
Target string `json:"target_step_id"`
|
|
}
|
|
type canvasAIPlan struct {
|
|
Steps []canvasAIStepPlan `json:"steps"`
|
|
Edges []canvasAIEdgePlan `json:"edges"`
|
|
Versions map[string]int64 `json:"frozen_versions"`
|
|
}
|
|
|
|
func canvasNodeKey(board, node string) string { return board + "/" + node }
|
|
|
|
func buildCanvasAIPlan(snapshots map[string]canvasSnapshot, board, node string, propagate bool) (canvasAIPlan, error) {
|
|
plan := canvasAIPlan{Steps: []canvasAIStepPlan{}, Edges: []canvasAIEdgePlan{}, Versions: map[string]int64{}}
|
|
data := map[string]map[string]any{}
|
|
for board, snapshot := range snapshots {
|
|
plan.Versions[board] = snapshot.Version
|
|
for node, d := range canvasNodeData(snapshot.Document) {
|
|
data[canvasNodeKey(board, node)] = d
|
|
}
|
|
}
|
|
var resolve func(string, string, string, string, int) (string, error)
|
|
resolve = func(board, node, handle, direction string, depth int) (string, error) {
|
|
if depth > 8 {
|
|
return "", errors.New("graph_limit_exceeded")
|
|
}
|
|
d := data[canvasNodeKey(board, node)]
|
|
if d == nil {
|
|
return "", errGraphOwnership
|
|
}
|
|
if d["kind"] != "frame" {
|
|
if !canvasChoice(d["kind"], "text", "script", "plain-text", "image", "plain-image") {
|
|
return "", errGraphOwnership
|
|
}
|
|
return canvasNodeKey(board, node), nil
|
|
}
|
|
if !strings.HasPrefix(handle, "port:") {
|
|
return "", errGraphOwnership
|
|
}
|
|
sub, _ := d["subgraph"].(map[string]any)
|
|
child, _ := sub["boardId"].(string)
|
|
ports, _ := d["ports"].([]any)
|
|
for _, v := range ports {
|
|
p := v.(map[string]any)
|
|
if p["id"] == strings.TrimPrefix(handle, "port:") && p["direction"] == direction {
|
|
target, _ := p["nodeId"].(string)
|
|
h, _ := p["handle"].(string)
|
|
return resolve(child, target, h, direction, depth+1)
|
|
}
|
|
}
|
|
return "", errGraphOwnership
|
|
}
|
|
outgoing := map[string][]canvasAIEdgePlan{}
|
|
allIncoming := map[string][]canvasAIEdgePlan{}
|
|
for board, snapshot := range snapshots {
|
|
var doc map[string]any
|
|
_ = json.Unmarshal(snapshot.Document, &doc)
|
|
for _, v := range doc["edges"].([]any) {
|
|
e := v.(map[string]any)
|
|
d, _ := e["data"].(map[string]any)
|
|
execution, _ := d["execution"].(map[string]any)
|
|
if execution["enabled"] == false {
|
|
continue
|
|
}
|
|
source, _ := e["source"].(string)
|
|
sh, _ := e["sourceHandle"].(string)
|
|
target, _ := e["target"].(string)
|
|
th, _ := e["targetHandle"].(string)
|
|
a, err := resolve(board, source, sh, "output", 0)
|
|
if err != nil {
|
|
return plan, err
|
|
}
|
|
b, err := resolve(board, target, th, "input", 0)
|
|
if err != nil {
|
|
return plan, err
|
|
}
|
|
edge := canvasAIEdgePlan{ID: canvasNodeKey(board, e["id"].(string)), Source: a, Target: b}
|
|
outgoing[a] = append(outgoing[a], edge)
|
|
allIncoming[b] = append(allIncoming[b], edge)
|
|
}
|
|
}
|
|
root := canvasNodeKey(board, node)
|
|
if data[root] == nil || !canvasChoice(data[root]["kind"], "text", "script", "plain-text", "image", "plain-image") {
|
|
return plan, errGraphOwnership
|
|
}
|
|
for key := range outgoing {
|
|
sort.Slice(outgoing[key], func(i, j int) bool { return outgoing[key][i].ID < outgoing[key][j].ID })
|
|
}
|
|
selected := map[string]bool{}
|
|
skipped := map[string]bool{}
|
|
queue := []string{root}
|
|
for len(queue) > 0 {
|
|
id := queue[0]
|
|
queue = queue[1:]
|
|
if selected[id] {
|
|
continue
|
|
}
|
|
selected[id] = true
|
|
if len(selected) > 64 {
|
|
return plan, errGraphCapacity
|
|
}
|
|
if id != root {
|
|
ai, _ := data[id]["ai"].(map[string]any)
|
|
model, _ := ai["modelId"].(string)
|
|
if ai["enabled"] != true || model == "" {
|
|
skipped[id] = true
|
|
continue
|
|
}
|
|
}
|
|
if !propagate {
|
|
continue
|
|
}
|
|
for _, edge := range outgoing[id] {
|
|
plan.Edges = append(plan.Edges, edge)
|
|
queue = append(queue, edge.Target)
|
|
}
|
|
}
|
|
// Kahn's algorithm waits for every participating predecessor at a diamond.
|
|
indegree := map[string]int{}
|
|
incoming := map[string][]string{}
|
|
depths := map[string]int{}
|
|
for _, edge := range plan.Edges {
|
|
indegree[edge.Target]++
|
|
incoming[edge.Target] = append(incoming[edge.Target], edge.Source)
|
|
}
|
|
ready := []string{}
|
|
for id := range selected {
|
|
if indegree[id] == 0 {
|
|
ready = append(ready, id)
|
|
}
|
|
}
|
|
for len(ready) > 0 {
|
|
sort.Strings(ready)
|
|
id := ready[0]
|
|
ready = ready[1:]
|
|
parts := strings.SplitN(id, "/", 2)
|
|
d := data[id]
|
|
ai, _ := d["ai"].(map[string]any)
|
|
model, _ := ai["modelId"].(string)
|
|
status := "queued"
|
|
if skipped[id] {
|
|
status = "skipped"
|
|
}
|
|
content := canvasFrozenContent(d)
|
|
static := []canvasAIStaticInput{}
|
|
sort.Slice(allIncoming[id], func(i, j int) bool { return allIncoming[id][i].ID < allIncoming[id][j].ID })
|
|
for _, edge := range allIncoming[id] {
|
|
if propagate && selected[edge.Source] && !skipped[edge.Source] {
|
|
continue
|
|
}
|
|
origin := strings.SplitN(edge.Source, "/", 2)
|
|
source := data[edge.Source]
|
|
static = append(static, canvasAIStaticInput{EdgeID: edge.ID, BoardID: origin[0], NodeID: origin[1], Kind: source["kind"].(string), Content: canvasFrozenContent(source)})
|
|
}
|
|
dependencies := incoming[id]
|
|
if dependencies == nil {
|
|
dependencies = []string{}
|
|
}
|
|
sort.Strings(dependencies)
|
|
plan.Steps = append(plan.Steps, canvasAIStepPlan{StaticInputs: static, ID: id, BoardID: parts[0], NodeID: parts[1], Kind: d["kind"].(string), ModelID: model, Config: ai, FrozenContent: content, Fingerprint: canvasTargetFingerprint(parts[0], parts[1], d), Status: status, Dependencies: dependencies})
|
|
plan.Versions[parts[0]] = snapshots[parts[0]].Version
|
|
for _, edge := range outgoing[id] {
|
|
if !propagate || skipped[id] {
|
|
continue
|
|
}
|
|
depth := depths[id] + 1
|
|
if depth > depths[edge.Target] {
|
|
depths[edge.Target] = depth
|
|
}
|
|
if depth > 32 {
|
|
return plan, errGraphCapacity
|
|
}
|
|
indegree[edge.Target]--
|
|
if indegree[edge.Target] == 0 {
|
|
ready = append(ready, edge.Target)
|
|
}
|
|
}
|
|
}
|
|
if len(plan.Steps) != len(selected) {
|
|
return plan, errors.New("execution_cycle")
|
|
}
|
|
sort.Slice(plan.Edges, func(i, j int) bool { return plan.Edges[i].ID < plan.Edges[j].ID })
|
|
return plan, nil
|
|
}
|
|
|
|
func canvasFrozenContent(data map[string]any) map[string]any {
|
|
if canvasChoice(data["kind"], "image", "plain-image") {
|
|
return map[string]any{"src": data["src"], "assetId": data["assetId"]}
|
|
}
|
|
return map[string]any{"text": data["text"]}
|
|
}
|