Files
ragflow/internal/agent/dsl/testdata/agent_msg.json
Zhichang Yu 3f805a64f1 feat(agent): align Go agent behavior with Python (except retrieval component) (#16225)
## Summary

Aligns the **Go agent runtime/canvas/components/tools** behavior with
the **Python `agent/` implementation** so the same stored canvas DSL
produces the same execution result on either side. Every component,
tool, and runtime primitive in `internal/agent/` is now driven by the
same semantics as its Python counterpart — variable resolution, template
substitution, control flow, error reporting, retry/cancel, and stream
event shapes.

The **retrieval component is the one explicit exception** in this PR. It
is being reworked in a separate change and is excluded from this
alignment pass; the wrapper slot (`universe_a_wrappers.go →
newRetrievalComponent`) is preserved.

## Scope of alignment

### Components (all aligned with `agent/component/`)
`Begin` · `Message` · `LLM` (incl. ChatTemplateKwargs,
MessageHistoryWindowSize, VisualFiles, Cite, OutputStructure,
JSONOutput, TopP, MaxRetries, DelayAfterError, credentials) · `Agent`
(react + tool artifact capture + `Reset()` interface-assert) · `Switch`
(12/12 operators, Python-equivalent semantics) · `Categorize` · `Invoke`
· `Iteration` · `Loop` (macro-expansion through `workflowx.AddLoopNode`)
· `UserFillUp` (Python-equivalent interrupt/resume via eino
`compose.Interrupt`/`ResumeWithData`) · `FillUp` · `DataOperations` ·
`ListOperations` · `StringTransform` · `VariableAggregator` ·
`VariableAssigner` · `Browser` (full stagehand runtime parity) ·
`DocsGenerator` · `ExcelProcessor`.

### Tools (all aligned with `agent/tools/`)
`Retrieval` (wrapper slot only — logic out of scope) · `MCPToolAdapter`
(streamable-HTTP) · `CodeExec` (sandbox bridge with
`code_exec_contract.go` matching Python contract) · `AkShare` · `ArXiv`
· `Crawler` · `DeepL` · `DuckDuckGo` · `Email` · `ExeSQL` · `GitHub` ·
`Google` · `GoogleScholar` · `Jin10` · `PubMed` · `QWeather` · `SearXNG`
· `Tavily` · `Tushare` · `Wencai` · `Wikipedia` · `YahooFinance` —
uniform `eino tool.InvokableTool` interface, SSRF protection, shared
HTTP client.

### Canvas execution engine (`internal/agent/canvas/`)
Aligned with Python's `agent/canvas.py`:
- **Scheduler** (`scheduler.go`): state pre/post handlers, node lambdas,
per-component timeout resolver (4-level: per-class env → per-class table
→ uniform env → 600s fallback), `legacyNoOpNames`.
- **Loop subgraph** (`loop_subgraph.go`): Python-equivalent
`AddLoopNode` macro expansion + condition translation.
- **Multibranch** (`multibranch.go`): `Switch` / `Categorize` routing
via `compose.NewGraphMultiBranch` — same branch selection semantics as
Python.
- **Parallel subgraph** (`parallel_subgraph.go`): matches Python's
parallel fan-out contract.
- **Interrupt/Resume** (`interrupt_resume.go`): `UserFillUpNodeBody` /
`IsInterruptError` / `ExtractInterruptContexts` — replaces the
deprecated Python sentinel chain with eino's native interrupt API,
preserving the same external behavior.
- **Checkpoint** (`checkpoint_store.go`): `RedisCheckPointStore`
Get/Set/Delete, with business metadata (status / canvas_id /
parent_run_id) on a parallel Redis Hash.
- **RunTracker** (`run_tracker.go`): Start / MarkSucceeded / MarkFailed
/ MarkCancelled / AttachCheckpoint — same lifecycle as the Python run
record.
- **Cancel** (`cancel.go`): Redis pub/sub watch.
- **Stream** (`stream.go`): SSE channel with `messages` / `waiting` /
`errors` / `done` events, same shape as Python's `agent.canvas.RunEvent`
payload.

### DSL bridge (`internal/agent/dsl/`)
- `normalize.go`: v1↔v2 collapsed into a single wire format — Python and
Go consume the same stored JSON.
- `reset.go`: per-run state reset matches Python's `Canvas.reset()`
semantics.
- Testdata mirrors Python's `agent_msg.json` / `all.json` / etc.

### Runtime (`internal/agent/runtime/`)
- `CanvasState` / `NewCanvasState` / `GetVar` / `SetVar` / `ReadVars`:
same `{{cpn_id@param}}` resolution model.
- `ResolveTemplate` (regex fast path + gonja fallback) — Python
Jinja-style semantics.
- `selector.go`, `metrics.go`, `component.go`: shared runtime contracts.

## Out of scope (intentionally)

- **`Retrieval` component logic** — wrapped only; full parity lands in a
follow-up PR.
- **Frontend** — only minor dsl-bridge / canvas UX fixes ride along.
- **CLI / admin / model registry** — orthogonal to agent behavior.

## How alignment is verified

`internal/service/agent_run_e2e_test.go` exercises the **full production
chain** against real Python-shaped DSL fixtures:
```
loadCanvasForUser → versionDAO.GetLatest → decodeCanvasFromDSL →
canvas.Compile → cc.Workflow.Invoke → answer extraction
```
using in-memory SQLite + miniredis (no Docker). Covers:
- `TestRunAgent_RealCanvas_BeginMessage` — happy path, `{{sys.query}}`
resolution
- `TestRunAgent_RealCanvas_WaitForUserResume` — two-run resume cycle
(Python-equivalent)
- `TestRunAgent_RealCanvas_CompileFails` — unknown component name →
sanitized error (Python-equivalent)
- `TestRunAgent_RealCanvas_InvokeFails` — unresolvable template ref
(Python-equivalent)
- `TestRunAgent_RunTracker_AttachCheckpoint_CallSequence` —
Start→AttachCheckpoint→MarkSucceeded lifecycle

`internal/handler/agent_test.go` — SSE streaming parity (`Content-Type:
text/event-stream`, `data: {…}\n\n`, trailing `data: [DONE]\n\n`,
OpenAI-compatible non-stream `choices`).

`internal/agent/canvas/fixture_compile_test.go` + per-component tests
pin the Python-equivalent outputs.

```
go test -count=1 -v -run 'TestRunAgent_RealCanvas|TestRunAgent_RunTracker' ./internal/service/
```

## Design reference

`docs/develop/agent-go-port-design.md` (1329 lines, last cross-checked
2026-06-17) — module layout, per-component / per-tool inventory,
corner-case catalogue, and the actionable backlog (Section 14, including
the retrieval alignment follow-up).

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-22 11:58:29 +08:00

228 lines
6.7 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"components": {
"Agent:TenderSpidersStick": {
"downstream": [
"Message:MajorNumbersPlay"
],
"obj": {
"component_name": "Agent",
"params": {
"cite": true,
"delay_after_error": 1,
"description": "",
"exception_default_value": "",
"exception_goto": [],
"exception_method": "",
"frequencyPenaltyEnabled": true,
"frequency_penalty": 0.7,
"llm_id": "glm-4.7-flashx@zz@ZHIPU-AI",
"maxTokensEnabled": false,
"max_retries": 3,
"max_rounds": 1,
"max_tokens": 256,
"mcp": [],
"message_history_window_size": 12,
"outputs": {
"content": {
"type": "string",
"value": ""
}
},
"presencePenaltyEnabled": true,
"presence_penalty": 0.4,
"prompts": [
{
"content": "{sys.query}",
"role": "user"
}
],
"showStructuredOutput": false,
"sys_prompt": "\n <role>\n You are a helpful assistant, an AI assistant specialized in problem-solving for the user.\n If a specific domain is provided, adapt your expertise to that domain; otherwise, operate as a generalist.\n </role>\n <instructions>\n 1. Understand the users request.\n 2. Decompose it into logical subtasks.\n 3. Execute each subtask step by step, reasoning transparently.\n 4. Validate accuracy and consistency.\n 5. Summarize the final result clearly.\n </instructions>",
"temperature": 0.1,
"temperatureEnabled": true,
"tools": [],
"topPEnabled": true,
"top_p": 0.3,
"user_prompt": "",
"visual_files_var": ""
}
},
"upstream": [
"begin"
]
},
"Message:MajorNumbersPlay": {
"downstream": [],
"obj": {
"component_name": "Message",
"params": {
"content": [
"{Agent:TenderSpidersStick@content}"
]
}
},
"upstream": [
"Agent:TenderSpidersStick"
]
},
"begin": {
"downstream": [
"Agent:TenderSpidersStick"
],
"obj": {
"component_name": "Begin",
"params": {
"mode": "conversational",
"prologue": "Hi! I'm your assistant. What can I do for you?"
}
},
"upstream": []
}
},
"globals": {
"sys.conversation_turns": 0,
"sys.date": "",
"sys.files": [],
"sys.history": [],
"sys.query": "",
"sys.user_id": ""
},
"graph": {
"edges": [
{
"id": "xy-edge__beginstart-Agent:TenderSpidersStickend",
"source": "begin",
"sourceHandle": "start",
"target": "Agent:TenderSpidersStick",
"targetHandle": "end"
},
{
"data": {
"isHovered": false
},
"id": "xy-edge__Agent:TenderSpidersStickstart-Message:MajorNumbersPlayend",
"source": "Agent:TenderSpidersStick",
"sourceHandle": "start",
"target": "Message:MajorNumbersPlay",
"targetHandle": "end"
}
],
"nodes": [
{
"data": {
"form": {
"mode": "conversational",
"prologue": "Hi! I'm your assistant. What can I do for you?"
},
"label": "Begin",
"name": "begin"
},
"dragging": false,
"id": "begin",
"measured": {
"height": 81,
"width": 200
},
"position": {
"x": -173.77599999999995,
"y": -145.59999999999994
},
"selected": false,
"sourcePosition": "left",
"targetPosition": "right",
"type": "beginNode"
},
{
"data": {
"form": {
"cite": true,
"delay_after_error": 1,
"description": "",
"exception_default_value": "",
"exception_goto": [],
"exception_method": "",
"frequencyPenaltyEnabled": true,
"frequency_penalty": 0.7,
"llm_id": "glm-4.7-flashx@zz@ZHIPU-AI",
"maxTokensEnabled": false,
"max_retries": 3,
"max_rounds": 1,
"max_tokens": 256,
"mcp": [],
"message_history_window_size": 12,
"outputs": {
"content": {
"type": "string",
"value": ""
}
},
"presencePenaltyEnabled": true,
"presence_penalty": 0.4,
"prompts": [
{
"content": "{sys.query}",
"role": "user"
}
],
"showStructuredOutput": false,
"sys_prompt": "\n <role>\n You are a helpful assistant, an AI assistant specialized in problem-solving for the user.\n If a specific domain is provided, adapt your expertise to that domain; otherwise, operate as a generalist.\n </role>\n <instructions>\n 1. Understand the users request.\n 2. Decompose it into logical subtasks.\n 3. Execute each subtask step by step, reasoning transparently.\n 4. Validate accuracy and consistency.\n 5. Summarize the final result clearly.\n </instructions>",
"temperature": 0.1,
"temperatureEnabled": true,
"tools": [],
"topPEnabled": true,
"top_p": 0.3,
"user_prompt": "",
"visual_files_var": ""
},
"label": "Agent",
"name": "Agent_0"
},
"dragging": false,
"id": "Agent:TenderSpidersStick",
"measured": {
"height": 79,
"width": 200
},
"position": {
"x": 92.03339543909598,
"y": -132.31937403691842
},
"selected": true,
"sourcePosition": "right",
"targetPosition": "left",
"type": "agentNode"
},
{
"data": {
"form": {
"content": [
"{Agent:TenderSpidersStick@content}"
]
},
"label": "Message",
"name": "Message_0"
},
"dragging": false,
"id": "Message:MajorNumbersPlay",
"measured": {
"height": 85,
"width": 200
},
"position": {
"x": 336.5453954390959,
"y": -120.22337403691841
},
"selected": false,
"sourcePosition": "right",
"targetPosition": "left",
"type": "messageNode"
}
]
},
"history": [],
"memory": [],
"messages": [],
"path": [],
"retrieval": [],
"variables": []
}