mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-12 03:33:33 +08:00
fix: agent chat completions can not use (#16570)
### Summary As title <img width="2370" height="2039" alt="image" src="https://github.com/user-attachments/assets/4cccf543-3908-49ee-8101-c5068fbf53ec" />
This commit is contained in:
@@ -154,30 +154,22 @@ func WriteDoneFrame(w http.ResponseWriter) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteChatbotRunEvent translates one canvas.RunEvent into the
|
||||
// unified python-shaped chat-completion envelope (same shape as
|
||||
// WriteChatbotFrame). This unifies the SSE format across:
|
||||
// WriteChatbotRunEvent translates one canvas.RunEvent into the flat
|
||||
// Python agent-canvas SSE envelope:
|
||||
//
|
||||
// - /api/v1/agents/chat/completions (was: writeChatCompletionSSE)
|
||||
// - /api/v1/agentbots/<id>/completions (was: WriteChatbotFrame per-event)
|
||||
// data: {"event":"message","message_id":"...","task_id":"...",
|
||||
// "session_id":"...","created_at":123,"data":{"content":"..."}}\n\n
|
||||
//
|
||||
// This is intentionally different from WriteChatbotFrame's legacy
|
||||
// chatbot `{code,data:{answer:"..."}}` shape. The agent React page's
|
||||
// use-send-message.ts parser appends each parsed object directly to
|
||||
// answerList and expects top-level `event` / `message_id`, plus a
|
||||
// typed `data` payload. If RunEvent frames are double-wrapped in
|
||||
// data.answer, the browser receives bytes but cannot render the
|
||||
// assistant message or correlate the current Log panel.
|
||||
//
|
||||
// The "done" event type emits `data: [DONE]\n\n` (no envelope),
|
||||
// matching the OpenAI-style terminator and the existing
|
||||
// AgentbotCompletion wire.
|
||||
//
|
||||
// For non-done events, ev.Data is placed verbatim into the `answer`
|
||||
// field — callers pass canvas-runner output that is itself a JSON
|
||||
// string (e.g. `{"answer":"hi back","reference":[]}`); the iframe
|
||||
// SDK then JSON.parse()s the `answer` string to extract the inner
|
||||
// fields. This matches the existing AgentbotCompletion behaviour.
|
||||
//
|
||||
// The event type is forwarded as the `event` field of the envelope
|
||||
// (PR #14589) so the front-end can distinguish interactive
|
||||
// `user_inputs` / `workflow_finished` events from plain `message`
|
||||
// streams and render the UserFillUp form vs the assistant text.
|
||||
// Without this field the form UI never appears because the
|
||||
// iframe SDK has no way to know the canvas paused for human
|
||||
// input.
|
||||
// matching the Python agent API terminator.
|
||||
//
|
||||
// Returns the write error so callers can short-circuit; both nil
|
||||
// and io.ErrClosedPipe are tolerated because the client may have
|
||||
@@ -193,13 +185,65 @@ func WriteChatbotRunEvent(w http.ResponseWriter, ev canvas.RunEvent) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
f := ChatbotSSEFrame{
|
||||
Event: ev.Type,
|
||||
Data: ev.Data,
|
||||
Reference: map[string]any{},
|
||||
SessionID: ev.SessionID,
|
||||
|
||||
var data any = map[string]any{}
|
||||
if ev.Data != "" {
|
||||
if err := json.Unmarshal([]byte(ev.Data), &data); err != nil {
|
||||
data = ev.Data
|
||||
}
|
||||
}
|
||||
return WriteChatbotFrame(w, f)
|
||||
if ev.Type == "error" {
|
||||
msg := "an internal error occurred"
|
||||
if m, ok := data.(map[string]any); ok {
|
||||
if s, _ := m["message"].(string); s != "" {
|
||||
msg = s
|
||||
}
|
||||
}
|
||||
payload := map[string]any{
|
||||
"code": 500,
|
||||
"message": msg,
|
||||
"data": false,
|
||||
}
|
||||
return writeSSEJSON(w, payload)
|
||||
}
|
||||
|
||||
payload := map[string]any{
|
||||
"data": data,
|
||||
"created_at": ev.CreatedAt,
|
||||
}
|
||||
if ev.Type != "" {
|
||||
payload["event"] = ev.Type
|
||||
}
|
||||
if ev.MessageID != "" {
|
||||
payload["message_id"] = ev.MessageID
|
||||
}
|
||||
if ev.TaskID != "" {
|
||||
payload["task_id"] = ev.TaskID
|
||||
}
|
||||
if ev.SessionID != "" {
|
||||
payload["session_id"] = ev.SessionID
|
||||
}
|
||||
return writeSSEJSON(w, payload)
|
||||
}
|
||||
|
||||
func writeSSEJSON(w http.ResponseWriter, payload map[string]any) error {
|
||||
b, err := runtime.SafeJSONMarshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Write([]byte("data:")); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Write([]byte("\n\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
if flusher, ok := w.(http.Flusher); ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AgentbotSSEFrame mirrors ChatbotSSEFrame for the agentbot
|
||||
|
||||
@@ -249,6 +249,8 @@ func TestWriteChatbotRunEvent_UserInputsEvent(t *testing.T) {
|
||||
rec := &recordingResponseWriter{header: http.Header{}}
|
||||
if err := WriteChatbotRunEvent(rec, canvas.RunEvent{
|
||||
Type: "user_inputs",
|
||||
MessageID: "msg-1",
|
||||
TaskID: "task-1",
|
||||
Data: `{"components":[{"id":"email","type":"text","required":true}]}`,
|
||||
SessionID: "sess-1",
|
||||
}); err != nil {
|
||||
@@ -258,9 +260,18 @@ func TestWriteChatbotRunEvent_UserInputsEvent(t *testing.T) {
|
||||
if !strings.Contains(body, `"event":"user_inputs"`) {
|
||||
t.Errorf("body missing event=user_inputs: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"message_id":"msg-1"`) {
|
||||
t.Errorf("body missing message_id: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"task_id":"task-1"`) {
|
||||
t.Errorf("body missing task_id: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"session_id":"sess-1"`) {
|
||||
t.Errorf("body missing session_id: %s", body)
|
||||
}
|
||||
if strings.Contains(body, `"answer":"`) {
|
||||
t.Errorf("body should not wrap run events in data.answer: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWriteChatbotRunEvent_WorkflowFinishedEvent covers the second
|
||||
@@ -270,7 +281,7 @@ func TestWriteChatbotRunEvent_WorkflowFinishedEvent(t *testing.T) {
|
||||
rec := &recordingResponseWriter{header: http.Header{}}
|
||||
if err := WriteChatbotRunEvent(rec, canvas.RunEvent{
|
||||
Type: "workflow_finished",
|
||||
Data: `{"answer":"done"}`,
|
||||
Data: `{"outputs":"done"}`,
|
||||
SessionID: "sess-2",
|
||||
}); err != nil {
|
||||
t.Fatalf("WriteChatbotRunEvent: %v", err)
|
||||
@@ -279,6 +290,9 @@ func TestWriteChatbotRunEvent_WorkflowFinishedEvent(t *testing.T) {
|
||||
if !strings.Contains(body, `"event":"workflow_finished"`) {
|
||||
t.Errorf("body missing event=workflow_finished: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"outputs":"done"`) {
|
||||
t.Errorf("body missing workflow output payload: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWriteChatbotRunEvent_MessageEventCarriesEvent ensures the
|
||||
@@ -290,7 +304,8 @@ func TestWriteChatbotRunEvent_MessageEventCarriesEvent(t *testing.T) {
|
||||
rec := &recordingResponseWriter{header: http.Header{}}
|
||||
if err := WriteChatbotRunEvent(rec, canvas.RunEvent{
|
||||
Type: "message",
|
||||
Data: `{"answer":"hi"}`,
|
||||
MessageID: "msg-3",
|
||||
Data: `{"content":"hi"}`,
|
||||
SessionID: "sess-3",
|
||||
}); err != nil {
|
||||
t.Fatalf("WriteChatbotRunEvent: %v", err)
|
||||
@@ -299,6 +314,12 @@ func TestWriteChatbotRunEvent_MessageEventCarriesEvent(t *testing.T) {
|
||||
if !strings.Contains(body, `"event":"message"`) {
|
||||
t.Errorf("message frame should carry event=message: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"message_id":"msg-3"`) {
|
||||
t.Errorf("message frame should carry top-level message_id: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"content":"hi"`) {
|
||||
t.Errorf("message frame should carry data.content: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
// recordingResponseWriter is a minimal http.ResponseWriter stub
|
||||
|
||||
Reference in New Issue
Block a user