mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
f1dd30a388
Three independent bugs fixed: 1. IM channels were sending raw <attachments>/<reactions>/<speech> tag blocks alongside file attachments. Now ExtractAssistantOutputs strips these tags before building the outbound channel message. 2. WebUI rendered these tags as markdown after page refresh. Now extractMessageText strips agent tags for non-user messages. 3. WebUI lost attachment blocks after refresh because convertMessagesToChats did not call buildAssetBlocks when merging assistant messages into a pending tool-call group. Also made LinkOutboundAssets session-aware so assets are linked to the correct assistant message.
102 lines
2.3 KiB
Go
102 lines
2.3 KiB
Go
package flow
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/memohai/memoh/internal/agent"
|
|
"github.com/memohai/memoh/internal/conversation"
|
|
)
|
|
|
|
// ExtractAssistantOutputs collects assistant-role outputs from a slice of ModelMessages.
|
|
func ExtractAssistantOutputs(messages []conversation.ModelMessage) []conversation.AssistantOutput {
|
|
if len(messages) == 0 {
|
|
return nil
|
|
}
|
|
outputs := make([]conversation.AssistantOutput, 0, len(messages))
|
|
for _, msg := range messages {
|
|
if msg.Role != "assistant" {
|
|
continue
|
|
}
|
|
if hasToolCallContent(msg) {
|
|
continue
|
|
}
|
|
rawParts := msg.ContentParts()
|
|
parts := filterVisibleContentParts(rawParts)
|
|
content := visibleContentText(parts)
|
|
if len(rawParts) == 0 {
|
|
content = strings.TrimSpace(msg.TextContent())
|
|
}
|
|
if content == "" && len(parts) == 0 {
|
|
continue
|
|
}
|
|
content = agent.StripAgentTags(content)
|
|
outputs = append(outputs, conversation.AssistantOutput{Content: content, Parts: parts})
|
|
}
|
|
return outputs
|
|
}
|
|
|
|
func hasToolCallContent(msg conversation.ModelMessage) bool {
|
|
if len(msg.ToolCalls) > 0 {
|
|
return true
|
|
}
|
|
for _, p := range msg.ContentParts() {
|
|
if p.Type == "tool-call" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func filterVisibleContentParts(parts []conversation.ContentPart) []conversation.ContentPart {
|
|
if len(parts) == 0 {
|
|
return nil
|
|
}
|
|
filtered := make([]conversation.ContentPart, 0, len(parts))
|
|
for _, p := range parts {
|
|
if isVisibleContentPart(p) {
|
|
filtered = append(filtered, p)
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func isVisibleContentPart(part conversation.ContentPart) bool {
|
|
if !part.HasValue() {
|
|
return false
|
|
}
|
|
switch strings.ToLower(strings.TrimSpace(part.Type)) {
|
|
case "reasoning", "tool-call", "tool-result":
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func visibleContentText(parts []conversation.ContentPart) string {
|
|
if len(parts) == 0 {
|
|
return ""
|
|
}
|
|
texts := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
text := strings.TrimSpace(visibleContentPartText(part))
|
|
if text == "" {
|
|
continue
|
|
}
|
|
texts = append(texts, text)
|
|
}
|
|
return strings.TrimSpace(strings.Join(texts, "\n"))
|
|
}
|
|
|
|
func visibleContentPartText(part conversation.ContentPart) string {
|
|
if strings.TrimSpace(part.Text) != "" {
|
|
return part.Text
|
|
}
|
|
if strings.TrimSpace(part.URL) != "" {
|
|
return part.URL
|
|
}
|
|
if strings.TrimSpace(part.Emoji) != "" {
|
|
return part.Emoji
|
|
}
|
|
return ""
|
|
}
|