Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 11 additions & 46 deletions agentteams-controller/cmd/agt/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"strings"

"github.com/spf13/cobra"

"github.com/agentscope-ai/AgentTeams/agentteams-controller/internal/workflow"
)

func getCmd() *cobra.Command {
Expand Down Expand Up @@ -123,7 +125,15 @@ func getProjectsCmd() *cobra.Command {
return fmt.Errorf("get project workflow: %w", err)
}
if mermaid {
fmt.Println(workflowMermaid(resp))
var snap workflow.Snapshot
buf, err := json.Marshal(resp)
if err != nil {
return fmt.Errorf("encode workflow: %w", err)
}
if err := json.Unmarshal(buf, &snap); err != nil {
return fmt.Errorf("decode workflow: %w", err)
}
fmt.Println(workflow.RenderMermaid(&snap))
return nil
}
if output == "json" {
Expand Down Expand Up @@ -205,51 +215,6 @@ func listStr(v any) string {
return strings.Join(parts, ", ")
}

// workflowMermaid renders a workflow response as a Mermaid flowchart
// (flowchart LR), mirroring LangGraph's draw_mermaid helper. Status is
// appended to each node label; next/ready nodes are highlighted.
func workflowMermaid(resp map[string]any) string {
var b strings.Builder
b.WriteString("flowchart LR\n")
nodes, _ := resp["nodes"].([]any)
edges, _ := resp["edges"].([]any)
nextSet := map[string]bool{}
for _, n := range resp["next"].([]any) {
if id, ok := n.(string); ok {
nextSet[id] = true
}
}
for _, raw := range nodes {
m, ok := raw.(map[string]any)
if !ok {
continue
}
id := toStr(m["id"])
name := toStr(m["name"])
status := toStr(m["status"])
label := name
if status != "" {
label += ": " + status
}
style := ""
if nextSet[id] {
style = ":::ready"
}
fmt.Fprintf(&b, " %s[%q]%s\n", id, label, style)
}
for _, raw := range edges {
m, ok := raw.(map[string]any)
if !ok {
continue
}
src := toStr(m["source"])
dst := toStr(m["target"])
fmt.Fprintf(&b, " %s --> %s\n", src, dst)
}
b.WriteString(" classDef ready fill:#d4edda,stroke:#28a745;\n")
return b.String()
}

// toStr converts a JSON-decoded value to its string form for table output.
func toStr(v any) string {
if v == nil {
Expand Down
63 changes: 30 additions & 33 deletions agentteams-controller/cmd/agt/mermaid_test.go
Original file line number Diff line number Diff line change
@@ -1,48 +1,45 @@
package main

import (
"encoding/json"
"strings"
"testing"

"github.com/agentscope-ai/AgentTeams/agentteams-controller/internal/workflow"
)

func TestWorkflowMermaid(t *testing.T) {
resp := map[string]any{
"nodes": []any{
map[string]any{"id": "t1", "name": "Task 1", "status": "completed"},
map[string]any{"id": "t2", "name": "Task 2", "status": "delegated"},
},
"edges": []any{
map[string]any{"source": "t1", "target": "t2"},
},
"next": []any{"t2"},
}
out := workflowMermaid(resp)
// TestCLIMermaidPath covers the CLI integration path: a JSON-decoded
// workflow response (map[string]any, as fetched via DoJSON) is re-marshaled
// into workflow.Snapshot and rendered — the exact sequence --mermaid runs.
func TestCLIMermaidPath(t *testing.T) {
raw := []byte(`{
"nodes": [
{"id": "t1", "name": "Task 1", "status": "completed", "assignee": "@w1"},
{"id": "t2", "name": "Task 2", "status": "delegated"}
],
"edges": [{"source": "t1", "target": "t2"}],
"next": ["t2"]
}`)
var resp map[string]any
if err := json.Unmarshal(raw, &resp); err != nil {
t.Fatalf("decode: %v", err)
}
var snap workflow.Snapshot
buf, err := json.Marshal(resp)
if err != nil {
t.Fatalf("marshal: %v", err)
}
if err := json.Unmarshal(buf, &snap); err != nil {
t.Fatalf("re-decode: %v", err)
}
out := workflow.RenderMermaid(&snap)
if !strings.HasPrefix(out, "flowchart LR") {
t.Fatalf("expected flowchart header, got %q", out)
}
if !strings.Contains(out, `t1["Task 1: completed"]`) {
t.Fatalf("expected node t1 with status, got %q", out)
}
if !strings.Contains(out, "t1 --> t2") {
t.Fatalf("expected edge t1 --> t2, got %q", out)
if !strings.Contains(out, `t1["Task 1: completed"]:::completed`) {
t.Fatalf("expected node t1 with status class, got %q", out)
}
// ready node t2 gets the ::ready class
if !strings.Contains(out, `t2["Task 2: delegated"]:::ready`) {
t.Fatalf("expected ready class on t2, got %q", out)
}
if !strings.Contains(out, "classDef ready") {
t.Fatalf("expected ready classDef, got %q", out)
}
}

func TestWorkflowMermaid_EmptyGraph(t *testing.T) {
resp := map[string]any{
"nodes": []any{},
"edges": []any{},
"next": []any{},
}
out := workflowMermaid(resp)
if !strings.HasPrefix(out, "flowchart LR") {
t.Fatalf("expected flowchart header, got %q", out)
}
}
1 change: 1 addition & 0 deletions agentteams-controller/internal/server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ func NewHTTPServer(addr string, deps ServerDeps) *HTTPServer {
projectTaskNameFn := func(r *http.Request) string { return r.PathValue("id") + "/" + r.PathValue("taskId") }
mux.Handle("GET /api/v1/projects", mw.RequireAuthz(authpkg.ActionList, "project", nil)(http.HandlerFunc(projh.ListProjects)))
mux.Handle("GET /api/v1/projects/{id}/workflow", mw.RequireAuthz(authpkg.ActionGet, "project", projectNameFn)(http.HandlerFunc(projh.GetProjectWorkflow)))
mux.Handle("GET /api/v1/projects/{id}/tasks/{taskId}", mw.RequireAuthz(authpkg.ActionGet, "project", projectNameFn)(http.HandlerFunc(projh.GetTaskInspection)))
mux.Handle("GET /api/v1/projects/{id}/tasks/{taskId}/artifact", mw.RequireAuthz(authpkg.ActionGet, "project", projectNameFn)(http.HandlerFunc(projh.GetTaskArtifact)))
mux.Handle("GET /api/v1/projects/{id}/spawns", mw.RequireAuthz(authpkg.ActionGet, "project", projectNameFn)(http.HandlerFunc(projh.GetProjectSpawns)))
mux.Handle("GET /api/v1/projects/{id}/spawns/{sessionId}/messages", mw.RequireAuthz(authpkg.ActionGet, "project", projectNameFn)(http.HandlerFunc(projh.GetProjectSpawnMessages)))
Expand Down
Loading
Loading