mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 18:18:37 +00:00
fix(pipeline): deliver generated images to chat channels, not just session history (#1351)
Codex native image_generation output (state.Observe.AssistantImages) was persisted to workspace/media/ and attached to the assistant message's MediaRefs for session history, but never added to state.Tool.MediaResults — the only field that flows into RunResult.Media and from there into the outbound channel message. Generated images showed up in the web UI (which reads session history) but chat channels (Telegram, Zalo, Slack, ...) received text-only replies with no attachment. Append the persisted image refs to state.Tool.MediaResults right after they're attached to the assistant message, so they reach RunResult.Media via the existing pipeline plumbing and cmd/gateway_consumer_normal.go's appendMediaToOutbound — the same path create_image/send_file already use. No new delivery path needed.
This commit is contained in:
1 parent
016263eb84
commit
b9a69e355e
2 files changed
+100
No files matched your search
@@ -109,6 +109,17 @@ func (s *FinalizeStage) Execute(ctx context.Context, state *RunState) error {
|
||||
assistantMsg.MediaRefs = append(assistantMsg.MediaRefs, assistantImageRefs...)
|
||||
state.Messages.AppendPending(assistantMsg)
|
||||
|
||||
// Surface generated images (Codex image_generation_call) on MediaResults
|
||||
// too, so they reach RunResult.Media / outbound channel delivery — not just
|
||||
// session history (MediaRefs were already appended to the message above).
|
||||
for _, ref := range assistantImageRefs {
|
||||
mr := MediaResult{Path: ref.Path, ContentType: ref.MimeType, Prompt: ref.Prompt}
|
||||
if info, statErr := os.Stat(ref.Path); statErr == nil {
|
||||
mr.Size = info.Size()
|
||||
}
|
||||
state.Tool.MediaResults = append(state.Tool.MediaResults, mr)
|
||||
}
|
||||
|
||||
// 4. Flush remaining pending messages to session store
|
||||
historyCountBeforeFlush := len(state.Messages.History())
|
||||
pending := state.Messages.FlushPending()
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
)
|
||||
|
||||
// A model-generated image (Codex native image_generation) must reach
|
||||
// RunResult.Media — not just the assistant message's MediaRefs — otherwise
|
||||
// chat channels never receive it (only the web UI reads session history).
|
||||
func TestFinalizeStage_GeneratedImageReachesMediaResults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
imgPath := filepath.Join(dir, "generated.png")
|
||||
if err := os.WriteFile(imgPath, []byte("fake-png-bytes"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
deps := &PipelineDeps{
|
||||
FlushMessages: func(context.Context, string, []providers.Message) error { return nil },
|
||||
UpdateMetadata: func(context.Context, string, providers.Usage, int) error { return nil },
|
||||
// Simulates persistAssistantImages: real implementation decodes base64,
|
||||
// writes to workspace/media/, and appends MediaRefs to msg.
|
||||
PersistAssistantImages: func(msg *providers.Message, _ string) {
|
||||
msg.MediaRefs = append(msg.MediaRefs, providers.MediaRef{
|
||||
ID: "img-1",
|
||||
MimeType: "image/png",
|
||||
Kind: "image",
|
||||
Path: imgPath,
|
||||
Prompt: "a luxury jewelry poster",
|
||||
})
|
||||
msg.Images = nil
|
||||
},
|
||||
}
|
||||
|
||||
stage := NewFinalizeStage(deps)
|
||||
state := defaultState()
|
||||
state.Observe.FinalContent = "Here's the poster."
|
||||
state.Observe.AssistantImages = []providers.ImageContent{
|
||||
{MimeType: "image/png", Data: "ZmFrZS1wbmctYnl0ZXM="},
|
||||
}
|
||||
|
||||
if err := stage.Execute(context.Background(), state); err != nil {
|
||||
t.Fatalf("Execute() error: %v", err)
|
||||
}
|
||||
|
||||
if len(state.Tool.MediaResults) != 1 {
|
||||
t.Fatalf("MediaResults = %d entries, want 1 (generated image missing from outbound media)", len(state.Tool.MediaResults))
|
||||
}
|
||||
got := state.Tool.MediaResults[0]
|
||||
if got.Path != imgPath {
|
||||
t.Errorf("MediaResults[0].Path = %q, want %q", got.Path, imgPath)
|
||||
}
|
||||
if got.ContentType != "image/png" {
|
||||
t.Errorf("MediaResults[0].ContentType = %q, want image/png", got.ContentType)
|
||||
}
|
||||
if got.Prompt != "a luxury jewelry poster" {
|
||||
t.Errorf("MediaResults[0].Prompt = %q, want the generation prompt", got.Prompt)
|
||||
}
|
||||
if got.Size != int64(len("fake-png-bytes")) {
|
||||
t.Errorf("MediaResults[0].Size = %d, want %d (stat'd from disk)", got.Size, len("fake-png-bytes"))
|
||||
}
|
||||
}
|
||||
|
||||
// No generated images → no spurious MediaResults entries.
|
||||
func TestFinalizeStage_NoGeneratedImagesNoMediaResults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
deps := &PipelineDeps{
|
||||
FlushMessages: func(context.Context, string, []providers.Message) error { return nil },
|
||||
UpdateMetadata: func(context.Context, string, providers.Usage, int) error { return nil },
|
||||
}
|
||||
|
||||
stage := NewFinalizeStage(deps)
|
||||
state := defaultState()
|
||||
state.Observe.FinalContent = "just text, no image"
|
||||
|
||||
if err := stage.Execute(context.Background(), state); err != nil {
|
||||
t.Fatalf("Execute() error: %v", err)
|
||||
}
|
||||
if len(state.Tool.MediaResults) != 0 {
|
||||
t.Fatalf("MediaResults = %d entries, want 0", len(state.Tool.MediaResults))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user