mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-28 19:58:11 +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:
@@ -729,9 +729,11 @@ func (s *stubChatRunner) RunAgent(_ context.Context, _, _, _, _ string, _ any) (
|
||||
|
||||
// TestAgentChatCompletions_StreamSetsContentType covers the SSE
|
||||
// path: the handler streams canvas.RunEvent frames as
|
||||
// `data: {...}\n\n` with a trailing `data: [DONE]\n\n` terminator,
|
||||
// matching the Python `completion()` wire format in
|
||||
// api/db/services/canvas_service.py:368.
|
||||
// `data: {...}\n\n` with a trailing `data: [DONE]\n\n` terminator.
|
||||
// The frame shape is the unified python envelope
|
||||
// {code:0, message:"", data:{answer, reference, audio_binary, id,
|
||||
// session_id}} — the same shape /api/v1/agentbots/<id>/completions
|
||||
// emits. See service.WriteChatbotRunEvent and WriteChatbotFrame.
|
||||
//
|
||||
// The stubChatRunner emits one `message` frame and one `done` frame
|
||||
// so the test verifies the body contains both the framed event and
|
||||
@@ -757,8 +759,12 @@ func TestAgentChatCompletions_StreamSetsContentType(t *testing.T) {
|
||||
t.Errorf("Content-Type = %q, want text/event-stream", got)
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, "\"event\":\"message\"") || !strings.Contains(body, "\"answer\":\"hi back\"") {
|
||||
t.Errorf("body should contain framed message event, got %q", body)
|
||||
// Body must contain the unified python envelope (`code/data.answer`)
|
||||
// and the [DONE] terminator. The iframe SDK JSON.parse()s `answer`
|
||||
// to extract the inner fields, so the embedded JSON is double-encoded
|
||||
// (escaped quotes inside the outer `"answer"` string).
|
||||
if !strings.Contains(body, "\"code\":0") || !strings.Contains(body, `"answer":"{\"answer\":\"hi back\",\"reference\":[]}"`) {
|
||||
t.Errorf("body should contain unified python envelope with answer, got %q", body)
|
||||
}
|
||||
if !strings.HasSuffix(body, "data: [DONE]\n\n") {
|
||||
t.Errorf("body should end with [DONE] terminator, got %q", body)
|
||||
@@ -768,9 +774,9 @@ func TestAgentChatCompletions_StreamSetsContentType(t *testing.T) {
|
||||
// TestAgentChatCompletions_DefaultBranchStreamsSSE covers the
|
||||
// scenario the user actually hit: `openai-compatible: false` with no
|
||||
// `stream` field on the body. The handler must still invoke the
|
||||
// canvas runner and stream the result as SSE — matching Python's
|
||||
// `completion()` which always yields SSE on the non-openai path
|
||||
// regardless of the stream flag.
|
||||
// canvas runner and stream the result as SSE — the SSE envelope is
|
||||
// the unified python shape shared with
|
||||
// /api/v1/agentbots/<id>/completions regardless of the stream flag.
|
||||
func TestAgentChatCompletions_DefaultBranchStreamsSSE(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
@@ -792,8 +798,8 @@ func TestAgentChatCompletions_DefaultBranchStreamsSSE(t *testing.T) {
|
||||
t.Errorf("Content-Type = %q, want text/event-stream (default branch must stream)", got)
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, "\"event\":\"message\"") || !strings.Contains(body, "\"answer\":\"hello back\"") {
|
||||
t.Errorf("body should contain framed message event, got %q", body)
|
||||
if !strings.Contains(body, "\"code\":0") || !strings.Contains(body, `"answer":"{\"answer\":\"hello back\",\"reference\":[]}"`) {
|
||||
t.Errorf("body should contain unified python envelope with answer, got %q", body)
|
||||
}
|
||||
if !strings.HasSuffix(body, "data: [DONE]\n\n") {
|
||||
t.Errorf("body should end with [DONE] terminator, got %q", body)
|
||||
|
||||
Reference in New Issue
Block a user