mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 15:31:05 +08:00
Ports the agent canvas subsystem from Python to Go.
## What's included
### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages
### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |
### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7
### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)
### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs
### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
93 lines
3.3 KiB
Go
93 lines
3.3 KiB
Go
// Package canvas — Begin → Message e2e smoke test (Worker A, Phase 1).
|
|
//
|
|
// The simplest end-to-end compile+run path. Verifies:
|
|
//
|
|
// 1. BuildWorkflow returns a non-nil Workflow for a 2-node DSL.
|
|
// 2. Compile returns a CompiledCanvas.
|
|
// 3. The compiled Runnable.Invoke runs to completion (no eino wiring error).
|
|
// 4. The Message node's "{{sys.query}}" reference resolves against state
|
|
// that was seeded into Sys — even though our placeholder lambda doesn't
|
|
// actually emit a string, we exercise the variable resolution path by
|
|
// writing into Outputs via SetVar before Invoke.
|
|
//
|
|
// Real Begin/Message component bodies land in Phase 2 P0. Phase 1's
|
|
// placeholder lambdas echo the input map; the test therefore asserts the
|
|
// *plumbing* (compile, run, set/get state across nodes) without asserting
|
|
// component-specific semantics.
|
|
package canvas
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
)
|
|
|
|
// TestBeginToMessage_Smoke builds a Begin → Message DSL, seeds sys.query
|
|
// into state, and confirms the compiled workflow runs without error and
|
|
// the per-cpn Outputs bucket gets populated (proving the statePre/statePost
|
|
// handler chain works end-to-end).
|
|
func TestBeginToMessage_Smoke(t *testing.T) {
|
|
dsl := &Canvas{
|
|
Version: 1,
|
|
Components: map[string]CanvasComponent{
|
|
"begin_0": {
|
|
Obj: CanvasComponentObj{ComponentName: "Begin", Params: map[string]any{}},
|
|
Downstream: []string{"message_0"},
|
|
Upstream: []string{},
|
|
},
|
|
"message_0": {
|
|
Obj: CanvasComponentObj{ComponentName: "Message", Params: map[string]any{
|
|
"text": "hello {{sys.query}}",
|
|
}},
|
|
Downstream: []string{},
|
|
Upstream: []string{"begin_0"},
|
|
},
|
|
},
|
|
Path: []string{"begin_0", "message_0"},
|
|
}
|
|
|
|
cc, err := Compile(context.Background(), dsl)
|
|
if err != nil {
|
|
t.Fatalf("Compile: %v", err)
|
|
}
|
|
if cc.Workflow == nil {
|
|
t.Fatal("compiled Workflow is nil")
|
|
}
|
|
|
|
// Pre-seed state to mirror what the Begin node would normally inject.
|
|
// In Phase 1 we did this directly because no Begin body existed yet.
|
|
// With the real Begin component now registered (via the blank import
|
|
// in loop_semantics_test.go), Begin reads inputs["query"] and writes
|
|
// it into state.Sys["query"] itself — so we pass the query through
|
|
// the input map instead of seeding it directly, and Begin propagates
|
|
// it into the context-attached state.
|
|
runState := NewCanvasState("run-smoke", "task-smoke")
|
|
runState.SetVar("begin_0", "request", map[string]any{"q": "world"})
|
|
|
|
// Stash runState on the context so a hypothetical runner (Phase 5) can
|
|
// extract it via GetStateFromContext.
|
|
ctx := withState(context.Background(), runState)
|
|
|
|
// Invoke with the seed input. The "query" key flows into Begin's
|
|
// Invoke and is written to state.Sys["query"], where Message's
|
|
// ResolveTemplate of "{{sys.query}}" will read it.
|
|
in := map[string]any{"query": "world"}
|
|
out, err := cc.Workflow.Invoke(ctx, in)
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
if out == nil {
|
|
t.Fatal("Invoke returned nil output")
|
|
}
|
|
|
|
// Variable resolution: ResolveTemplate against the seeded state must
|
|
// produce "hello world" — this is what the real Message component will
|
|
// emit in Phase 2 P0.
|
|
got, err := ResolveTemplate("hello {{sys.query}}", runState)
|
|
if err != nil {
|
|
t.Fatalf("ResolveTemplate: %v", err)
|
|
}
|
|
if got != "hello world" {
|
|
t.Fatalf("template resolve: got %q want %q", got, "hello world")
|
|
}
|
|
}
|