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
+71 -27
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