mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
0730ff2945
These two fields controlled history context window (time-based) and token-based trimming. They are no longer needed — the resolver now always uses the hardcoded 24-hour default and skips token-based history trimming.
169 lines
3.8 KiB
Go
169 lines
3.8 KiB
Go
package flow
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
|
|
"github.com/memohai/memoh/internal/conversation"
|
|
"github.com/memohai/memoh/internal/db"
|
|
)
|
|
|
|
func sanitizeMessages(messages []conversation.ModelMessage) []conversation.ModelMessage {
|
|
cleaned := make([]conversation.ModelMessage, 0, len(messages))
|
|
for _, msg := range messages {
|
|
msg = normalizeUserMessageContent(msg)
|
|
if normalized, ok := normalizeImagePartsToDataURL(msg); ok {
|
|
msg = normalized
|
|
}
|
|
if strings.TrimSpace(msg.Role) == "" {
|
|
continue
|
|
}
|
|
if !msg.HasContent() && strings.TrimSpace(msg.ToolCallID) == "" {
|
|
continue
|
|
}
|
|
cleaned = append(cleaned, msg)
|
|
}
|
|
return cleaned
|
|
}
|
|
|
|
func normalizeImagePartsToDataURL(msg conversation.ModelMessage) (conversation.ModelMessage, bool) {
|
|
if len(msg.Content) == 0 {
|
|
return msg, false
|
|
}
|
|
var parts []map[string]json.RawMessage
|
|
if err := json.Unmarshal(msg.Content, &parts); err != nil || len(parts) == 0 {
|
|
return msg, false
|
|
}
|
|
|
|
changed := false
|
|
for i := range parts {
|
|
partTypeRaw, ok := parts[i]["type"]
|
|
if !ok {
|
|
continue
|
|
}
|
|
var partType string
|
|
if err := json.Unmarshal(partTypeRaw, &partType); err != nil || !strings.EqualFold(partType, "image") {
|
|
continue
|
|
}
|
|
|
|
imageRaw, ok := parts[i]["image"]
|
|
if !ok || len(imageRaw) == 0 {
|
|
continue
|
|
}
|
|
var tmp string
|
|
if json.Unmarshal(imageRaw, &tmp) == nil {
|
|
continue
|
|
}
|
|
|
|
var payload []byte
|
|
if b, ok := decodeIndexedByteObject(imageRaw); ok {
|
|
payload = b
|
|
} else if b, ok := decodeByteArray(imageRaw); ok {
|
|
payload = b
|
|
} else {
|
|
continue
|
|
}
|
|
if len(payload) == 0 {
|
|
continue
|
|
}
|
|
|
|
// action trigger to image only here.
|
|
mediaType := "application/octet-stream"
|
|
if mediaTypeRaw, ok := parts[i]["mediaType"]; ok {
|
|
var mt string
|
|
if err := json.Unmarshal(mediaTypeRaw, &mt); err == nil && strings.TrimSpace(mt) != "" {
|
|
mediaType = strings.TrimSpace(mt)
|
|
}
|
|
}
|
|
dataURL := "data:" + mediaType + ";base64," + base64.StdEncoding.EncodeToString(payload)
|
|
rebuilt, err := json.Marshal(dataURL)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
parts[i]["image"] = rebuilt
|
|
changed = true
|
|
}
|
|
|
|
if !changed {
|
|
return msg, false
|
|
}
|
|
rebuiltContent, err := json.Marshal(parts)
|
|
if err != nil {
|
|
return msg, false
|
|
}
|
|
msg.Content = rebuiltContent
|
|
return msg, true
|
|
}
|
|
|
|
func decodeByteArray(raw json.RawMessage) ([]byte, bool) {
|
|
var arr []int
|
|
if err := json.Unmarshal(raw, &arr); err != nil {
|
|
return nil, false
|
|
}
|
|
if len(arr) == 0 {
|
|
return nil, false
|
|
}
|
|
out := make([]byte, len(arr))
|
|
for i, v := range arr {
|
|
if v < 0 || v > 255 {
|
|
return nil, false
|
|
}
|
|
out[i] = byte(v)
|
|
}
|
|
return out, true
|
|
}
|
|
|
|
func decodeIndexedByteObject(raw json.RawMessage) ([]byte, bool) {
|
|
var obj map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &obj); err != nil || len(obj) == 0 {
|
|
return nil, false
|
|
}
|
|
type indexedByte struct {
|
|
idx int
|
|
val byte
|
|
}
|
|
items := make([]indexedByte, 0, len(obj))
|
|
for k, vRaw := range obj {
|
|
idx, err := strconv.Atoi(k)
|
|
if err != nil || idx < 0 {
|
|
return nil, false
|
|
}
|
|
var val int
|
|
if err := json.Unmarshal(vRaw, &val); err != nil || val < 0 || val > 255 {
|
|
return nil, false
|
|
}
|
|
items = append(items, indexedByte{idx: idx, val: byte(val)})
|
|
}
|
|
sort.Slice(items, func(i, j int) bool { return items[i].idx < items[j].idx })
|
|
for i := range items {
|
|
if items[i].idx != i {
|
|
return nil, false
|
|
}
|
|
}
|
|
out := make([]byte, len(items))
|
|
for i := range items {
|
|
out[i] = items[i].val
|
|
}
|
|
return out, true
|
|
}
|
|
|
|
func nonNilModelMessages(m []conversation.ModelMessage) []conversation.ModelMessage {
|
|
if m == nil {
|
|
return []conversation.ModelMessage{}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func parseResolverUUID(id string) (pgtype.UUID, error) {
|
|
if strings.TrimSpace(id) == "" {
|
|
return pgtype.UUID{}, errors.New("empty id")
|
|
}
|
|
return db.ParseUUID(id)
|
|
}
|