mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-27 02:42:00 +08:00
feat[Go]: port agent attachment download, chatbot + agentbot completion/info endpoints from Python (#16405)
## Summary
Ports five Python agent APIs to Go under the v1 Gin router:
- `GET /api/v1/agents/attachments/<attachment_id>/download`
- `POST /api/v1/chatbots/<dialog_id>/completions` (SSE)
- `GET /api/v1/chatbots/<dialog_id>/info`
- `POST /api/v1/agentbots/<agent_id>/completions` (SSE)
- `GET /api/v1/agentbots/<agent_id>/inputs`
Mirrors the existing Python wire shape (`{code, message,
data:{answer,reference,...}}` per Python `canvas_service.completion`) so
the iframe SDK and existing JS widgets keep working.
## Behavioural parity with Python
| # | Concern | How it's met |
|---|---------|--------------|
| R0 | Bot routes must not require regular user session | Routes mount
on `apiNoAuth` (router.go:198-202), with `BetaAuthMiddleware` only |
| R3 | Two SSE formats in Go drift | F2: `AgentChatCompletions` and
`AgentbotCompletion` share `service.WriteChatbotRunEvent` |
| R7 | `GetBySessionID` returns `(nil, nil)` on miss | Defensive
nil-check before `session.UserID != tenantID` |
| R8 | Begin component name vs ID | `FindBeginComponentID` resolves name
→ ID first, then `ExtractComponentInputForm(dsl, beginID)` |
| R9 | Defensive PromptConfig parsing | `stringFromMap` helper used for
`prologue` and `tavily_api_key` |
| R10 | `BetaAuthMiddleware` Bearer-prefix pre-filter | Removed —
`GetUserByToken` is called unconditionally, falls back to
`GetUserByBetaAPIToken` |
| F8 | Multi-turn chatbot history | `ChatbotCompletion` reads prior
turns from `session.Message`, appends user turn, calls LLM, persists new
pair via new `API4ConversationDAO.Update` |
| F9 | UUID gate stricter than plan | Removed — only `filepath.Base` +
CR/LF/quote header sanitization remains |
| H2 | Defence-in-depth IDOR | `AgentbotCompletion` calls `loadCanvas`
before delegating to `RunAgent` |
| M2 | SSE error leakage | `WriteChatbotFrame` emits generic `"an
internal error occurred"`; real error logged via `common.Error` |
## Verification
```bash
$ go vet ./... # clean (only pre-existing issues)
$ go build ./... # success
$ go test ./internal/handler/ ./internal/service/ ./internal/agent/dsl/ ./internal/common/ ./internal/dao/
ok ragflow/internal/handler 0.617s
ok ragflow/internal/service 1.729s
ok ragflow/internal/agent/dsl 0.008s
ok ragflow/internal/common 0.087s
ok ragflow/internal/dao 0.083s
```
1199 tests pass across 5 packages.
## Known follow-ups (out of scope for this PR)
- **F1**: token-level streaming in `ChatbotCompletion` (currently emits
one frame per turn)
- **F3**: per-route `auth_types` attribute in Go (currently applied via
route group middleware)
---------
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -391,9 +391,15 @@ func (h *AgentHandler) RunAgent(c *gin.Context) {
|
||||
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
||||
c.Writer.Header().Set("Cache-Control", "no-cache")
|
||||
c.Writer.Header().Set("Connection", "keep-alive")
|
||||
flusher, _ := c.Writer.(http.Flusher)
|
||||
for ev := range events {
|
||||
writeRunEventSSE(c.Writer, flusher, ev)
|
||||
if err := service.WriteChatbotRunEvent(c.Writer, ev); err != nil {
|
||||
common.Debug("agent run: client disconnected",
|
||||
zap.String("canvas_id", canvasID),
|
||||
zap.String("session_id", sessionID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,31 +429,6 @@ func readUserInput(c *gin.Context) string {
|
||||
return c.Query("user_input")
|
||||
}
|
||||
|
||||
// writeRunEventSSE writes one canvas.RunEvent as an SSE frame in the
|
||||
// Python envelope format (same as writeChatCompletionSSE):
|
||||
//
|
||||
// data:{"event":"<ev.Type>","message_id":"...","created_at":...,"task_id":"...","session_id":"...","data":<ev.Data>}
|
||||
//
|
||||
// The "done" type emits `data: [DONE]\n\n`.
|
||||
func writeRunEventSSE(w io.Writer, flusher http.Flusher, ev canvas.RunEvent) {
|
||||
if ev.Type == "done" {
|
||||
fmt.Fprintf(w, "data: [DONE]\n\n")
|
||||
if flusher != nil {
|
||||
flusher.Flush()
|
||||
}
|
||||
return
|
||||
}
|
||||
data := ev.Data
|
||||
if data == "" {
|
||||
data = "{}"
|
||||
}
|
||||
envelope := sseEnvelope(ev.Type, ev.MessageID, ev.CreatedAt, ev.TaskID, ev.SessionID, data)
|
||||
fmt.Fprintf(w, "data: %s\n\n", envelope)
|
||||
if flusher != nil {
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// sanitiseRunEventError passes through the error event payload
|
||||
// unchanged. The runner serialises canvas.ErrorEvent ({"message": ...})
|
||||
// before push, so when the payload round-trips through JSON the
|
||||
@@ -1066,17 +1047,19 @@ func (h *AgentHandler) AgentChatCompletions(c *gin.Context) {
|
||||
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
||||
c.Writer.Header().Set("Cache-Control", "no-cache")
|
||||
c.Writer.Header().Set("Connection", "keep-alive")
|
||||
flusher, _ := c.Writer.(http.Flusher)
|
||||
// SSE wire format mirrors Python's `completion()` at
|
||||
// api/db/services/canvas_service.py:368: each canvas event is one
|
||||
// `data: <json>\n\n` frame, and the channel close is signalled by
|
||||
// `data: [DONE]\n\n`. We do NOT emit an `event:` line — the
|
||||
// front-end's `use-send-message.ts` parser feeds each `data:` line
|
||||
// directly into JSON.parse and breaks on the `e` of `event:`
|
||||
// (browser console: "SyntaxError: Unexpected token 'e', \"event:
|
||||
// mes\"…"). The richer `writeRunEventSSE` helper still owns the
|
||||
// /api/v1/agents/{id}/run endpoint's wire format — see
|
||||
// writeRunEventSSE at agent.go for that path.
|
||||
// SSE wire format is the unified python envelope used by both
|
||||
// /api/v1/agents/chat/completions and /api/v1/agentbots/<id>/completions.
|
||||
// One frame per canvas event, all routed through
|
||||
// service.WriteChatbotRunEvent so the two paths share one writer
|
||||
// and one shape — see internal/service/bot_completion.go for the
|
||||
// frame definition. The same unified envelope is used by the
|
||||
// /api/v1/agents/{canvas_id}/run and /api/v1/agentbots/<id>/completions
|
||||
// endpoints, all going through service.WriteChatbotRunEvent. The
|
||||
// channel close is signalled by `data: [DONE]\n\n`. We do NOT emit
|
||||
// an SSE `event:` line — the front-end's `use-send-message.ts`
|
||||
// parser feeds each `data:` line directly into JSON.parse and
|
||||
// breaks on the `e` of `event:` (browser console: "SyntaxError:
|
||||
// Unexpected token 'e', \"event: mes\"…").
|
||||
for ev := range events {
|
||||
common.Debug("agent chat completions: streaming event",
|
||||
zap.String("agent_id", req.AgentID),
|
||||
@@ -1085,7 +1068,13 @@ func (h *AgentHandler) AgentChatCompletions(c *gin.Context) {
|
||||
zap.String("message_id", ev.MessageID),
|
||||
zap.String("task_id", ev.TaskID),
|
||||
)
|
||||
writeChatCompletionSSE(c.Writer, flusher, req.AgentID, ev)
|
||||
if err := service.WriteChatbotRunEvent(c.Writer, ev); err != nil {
|
||||
common.Debug("agent chat completions: client disconnected",
|
||||
zap.String("agent_id", req.AgentID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
common.Debug("agent chat completions: stream closed",
|
||||
zap.String("agent_id", req.AgentID),
|
||||
@@ -1093,53 +1082,6 @@ func (h *AgentHandler) AgentChatCompletions(c *gin.Context) {
|
||||
)
|
||||
}
|
||||
|
||||
// writeChatCompletionSSE emits one canvas.RunEvent in the
|
||||
// Python-shaped chat-completion SSE envelope:
|
||||
//
|
||||
// data:{"event":"<ev.Type>","message_id":"<ev.MessageID>","created_at":<ev.CreatedAt>,"task_id":"<ev.TaskID>","session_id":"<ev.SessionID>","data":<ev.Data>}
|
||||
//
|
||||
// The special "done" type sends `data: [DONE]\n\n` (no JSON envelope).
|
||||
func writeChatCompletionSSE(w io.Writer, flusher http.Flusher, agentID string, ev canvas.RunEvent) {
|
||||
if ev.Type == "done" {
|
||||
common.Debug("agent chat completions: writing done sentinel",
|
||||
zap.String("agent_id", agentID),
|
||||
zap.String("session_id", ev.SessionID),
|
||||
zap.String("task_id", ev.TaskID),
|
||||
)
|
||||
fmt.Fprint(w, "data: [DONE]\n\n")
|
||||
if flusher != nil {
|
||||
flusher.Flush()
|
||||
}
|
||||
return
|
||||
}
|
||||
data := ev.Data
|
||||
if data == "" {
|
||||
data = "{}"
|
||||
}
|
||||
common.Debug("agent chat completions: writing sse frame",
|
||||
zap.String("agent_id", agentID),
|
||||
zap.String("event_type", ev.Type),
|
||||
zap.String("message_id", ev.MessageID),
|
||||
zap.String("session_id", ev.SessionID),
|
||||
zap.String("task_id", ev.TaskID),
|
||||
)
|
||||
envelope := sseEnvelope(ev.Type, ev.MessageID, ev.CreatedAt, ev.TaskID, ev.SessionID, data)
|
||||
fmt.Fprintf(w, "data: %s\n\n", envelope)
|
||||
if flusher != nil {
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// sseEnvelope builds the Python-shaped SSE JSON payload:
|
||||
//
|
||||
// {"event":"<typ>","message_id":"<mid>","created_at":<ts>,"task_id":"<tid>","session_id":"<sid>","data":<raw>}
|
||||
func sseEnvelope(typ, mid string, ts int64, tid, sid, rawData string) string {
|
||||
return fmt.Sprintf(
|
||||
`{"event":%q,"message_id":%q,"created_at":%d,"task_id":%q,"session_id":%q,"data":%s}`,
|
||||
typ, mid, ts, tid, sid, rawData,
|
||||
)
|
||||
}
|
||||
|
||||
// RerunAgent POST /api/v1/agents/rerun — requires id, dsl, and
|
||||
// component_id. The Python agent API uses PipelineOperationLogService
|
||||
// and the dataflow queue, none of which the Go port has implemented
|
||||
|
||||
Reference in New Issue
Block a user