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:
Haruko386
2026-07-03 13:25:14 +08:00
committed by GitHub
parent e65bac238e
commit 383d059969
7 changed files with 353 additions and 71 deletions

View File

@@ -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

View File

@@ -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