mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
140 lines
4.9 KiB
Go
140 lines
4.9 KiB
Go
package flow
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// UserMessageMeta holds the structured metadata attached to every user
|
|
// message. It is the single source of truth for the YAML header sent to the LLM.
|
|
type UserMessageMeta struct {
|
|
MessageID string `json:"message-id,omitempty"`
|
|
ChannelIdentityID string `json:"channel-identity-id"`
|
|
DisplayName string `json:"display-name"`
|
|
Channel string `json:"channel"`
|
|
ConversationType string `json:"conversation-type"`
|
|
ConversationName string `json:"conversation-name,omitempty"`
|
|
Time string `json:"time"`
|
|
Timezone string `json:"timezone,omitempty"`
|
|
AttachmentPaths []string `json:"attachments"`
|
|
}
|
|
|
|
// BuildUserMessageMeta constructs a UserMessageMeta from the inbound parameters.
|
|
func BuildUserMessageMeta(messageID, channelIdentityID, displayName, channel, conversationType, conversationName string, attachmentPaths []string) UserMessageMeta {
|
|
if attachmentPaths == nil {
|
|
attachmentPaths = []string{}
|
|
}
|
|
return UserMessageMeta{
|
|
MessageID: messageID,
|
|
ChannelIdentityID: channelIdentityID,
|
|
DisplayName: displayName,
|
|
Channel: channel,
|
|
ConversationType: conversationType,
|
|
ConversationName: conversationName,
|
|
Time: time.Now().UTC().Format(time.RFC3339),
|
|
AttachmentPaths: attachmentPaths,
|
|
}
|
|
}
|
|
|
|
// BuildUserMessageMetaWithTime constructs metadata with an explicit timestamp
|
|
// and timezone label for user-facing prompts.
|
|
func BuildUserMessageMetaWithTime(messageID, channelIdentityID, displayName, channel, conversationType, conversationName string, attachmentPaths []string, now time.Time, timezone string) UserMessageMeta {
|
|
meta := BuildUserMessageMeta(messageID, channelIdentityID, displayName, channel, conversationType, conversationName, attachmentPaths)
|
|
if !now.IsZero() {
|
|
meta.Time = now.Format(time.RFC3339)
|
|
}
|
|
meta.Timezone = strings.TrimSpace(timezone)
|
|
return meta
|
|
}
|
|
|
|
// ToMap returns the metadata as a map with the same keys used in the YAML
|
|
// header, suitable for storing as inbox content JSONB.
|
|
func (m UserMessageMeta) ToMap() map[string]any {
|
|
result := map[string]any{
|
|
"channel-identity-id": m.ChannelIdentityID,
|
|
"display-name": m.DisplayName,
|
|
"channel": m.Channel,
|
|
"conversation-type": m.ConversationType,
|
|
"time": m.Time,
|
|
"attachments": m.AttachmentPaths,
|
|
}
|
|
if m.MessageID != "" {
|
|
result["message-id"] = m.MessageID
|
|
}
|
|
if m.ConversationName != "" {
|
|
result["conversation-name"] = m.ConversationName
|
|
}
|
|
if strings.TrimSpace(m.Timezone) != "" {
|
|
result["timezone"] = m.Timezone
|
|
}
|
|
return result
|
|
}
|
|
|
|
// FormatUserHeader wraps a user query with YAML front-matter metadata so
|
|
// the LLM sees structured context (sender, channel, time, attachments)
|
|
// alongside the raw message. This must be the single source of truth for
|
|
// user-message formatting — the agent gateway must NOT add its own header.
|
|
func FormatUserHeader(messageID, channelIdentityID, displayName, channel, conversationType, conversationName string, attachmentPaths []string, now time.Time, timezone, query string) string {
|
|
meta := BuildUserMessageMetaWithTime(messageID, channelIdentityID, displayName, channel, conversationType, conversationName, attachmentPaths, now, timezone)
|
|
return FormatUserHeaderFromMeta(meta, query)
|
|
}
|
|
|
|
// FormatUserHeaderFromMeta formats a pre-built UserMessageMeta into the
|
|
// YAML front-matter string sent to the LLM.
|
|
func FormatUserHeaderFromMeta(meta UserMessageMeta, query string) string {
|
|
var sb strings.Builder
|
|
sb.WriteString("---\n")
|
|
if meta.MessageID != "" {
|
|
writeYAMLString(&sb, "message-id", meta.MessageID)
|
|
}
|
|
writeYAMLString(&sb, "channel-identity-id", meta.ChannelIdentityID)
|
|
writeYAMLString(&sb, "display-name", meta.DisplayName)
|
|
writeYAMLString(&sb, "channel", meta.Channel)
|
|
writeYAMLString(&sb, "conversation-type", meta.ConversationType)
|
|
if meta.ConversationName != "" {
|
|
writeYAMLString(&sb, "conversation-name", meta.ConversationName)
|
|
}
|
|
writeYAMLString(&sb, "time", meta.Time)
|
|
if strings.TrimSpace(meta.Timezone) != "" {
|
|
writeYAMLString(&sb, "timezone", meta.Timezone)
|
|
}
|
|
if len(meta.AttachmentPaths) > 0 {
|
|
sb.WriteString("attachments:\n")
|
|
for _, p := range meta.AttachmentPaths {
|
|
sb.WriteString(" - ")
|
|
sb.WriteString(p)
|
|
sb.WriteByte('\n')
|
|
}
|
|
} else {
|
|
sb.WriteString("attachments: []\n")
|
|
}
|
|
sb.WriteString("---\n")
|
|
sb.WriteString(query)
|
|
return sb.String()
|
|
}
|
|
|
|
func writeYAMLString(sb *strings.Builder, key, value string) {
|
|
sb.WriteString(key)
|
|
sb.WriteString(": ")
|
|
if value == "" || needsYAMLQuote(value) {
|
|
sb.WriteByte('"')
|
|
sb.WriteString(strings.ReplaceAll(value, `"`, `\"`))
|
|
sb.WriteByte('"')
|
|
} else {
|
|
sb.WriteString(value)
|
|
}
|
|
sb.WriteByte('\n')
|
|
}
|
|
|
|
func needsYAMLQuote(s string) bool {
|
|
if s == "" {
|
|
return true
|
|
}
|
|
for _, c := range s {
|
|
if c == ':' || c == '#' || c == '"' || c == '\'' || c == '{' || c == '}' || c == '[' || c == ']' || c == ',' || c == '\n' {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|