mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
30653fbdbf
Pass replyTarget through the full pipeline (ChatRequest → gateway identity → agent headers → MCP session) so the send tool can detect when the destination matches the current conversation and return an error guiding the agent to reply directly instead.
264 lines
8.7 KiB
Go
264 lines
8.7 KiB
Go
// Package conversation defines conversation domain types and rules.
|
|
package conversation
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Conversation kind constants.
|
|
const (
|
|
KindDirect = "direct"
|
|
KindGroup = "group"
|
|
KindThread = "thread"
|
|
)
|
|
|
|
// Participant role constants.
|
|
const (
|
|
RoleOwner = "owner"
|
|
RoleAdmin = "admin"
|
|
RoleMember = "member"
|
|
)
|
|
|
|
// Conversation list access mode constants.
|
|
const (
|
|
AccessModeParticipant = "participant"
|
|
AccessModeChannelIdentityObserved = "channel_identity_observed"
|
|
)
|
|
|
|
// Conversation is the first-class conversation container.
|
|
type Conversation struct {
|
|
ID string `json:"id"`
|
|
BotID string `json:"bot_id"`
|
|
Kind string `json:"kind"`
|
|
ParentChatID string `json:"parent_chat_id,omitempty"`
|
|
Title string `json:"title,omitempty"`
|
|
CreatedBy string `json:"created_by"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// ConversationListItem is a conversation entry with access context for list rendering.
|
|
type ConversationListItem struct {
|
|
ID string `json:"id"`
|
|
BotID string `json:"bot_id"`
|
|
Kind string `json:"kind"`
|
|
ParentChatID string `json:"parent_chat_id,omitempty"`
|
|
Title string `json:"title,omitempty"`
|
|
CreatedBy string `json:"created_by"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
AccessMode string `json:"access_mode"`
|
|
ParticipantRole string `json:"participant_role,omitempty"`
|
|
LastObservedAt *time.Time `json:"last_observed_at,omitempty"`
|
|
}
|
|
|
|
// ConversationReadAccess is the resolved access context for reading conversation content.
|
|
type ConversationReadAccess struct {
|
|
AccessMode string
|
|
ParticipantRole string
|
|
LastObservedAt *time.Time
|
|
}
|
|
|
|
// Participant represents a chat member.
|
|
type Participant struct {
|
|
ChatID string `json:"chat_id"`
|
|
UserID string `json:"user_id"`
|
|
Role string `json:"role"`
|
|
JoinedAt time.Time `json:"joined_at"`
|
|
}
|
|
|
|
// Settings holds per-chat configuration.
|
|
type Settings struct {
|
|
ChatID string `json:"chat_id"`
|
|
ModelID string `json:"model_id,omitempty"`
|
|
}
|
|
|
|
// CreateRequest is the input for creating a bot-scoped conversation container.
|
|
type CreateRequest struct {
|
|
Kind string `json:"kind"`
|
|
Title string `json:"title,omitempty"`
|
|
ParentChatID string `json:"parent_chat_id,omitempty"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
}
|
|
|
|
// UpdateSettingsRequest is the input for updating chat settings.
|
|
type UpdateSettingsRequest struct {
|
|
ModelID *string `json:"model_id,omitempty"`
|
|
}
|
|
|
|
// ModelMessage is the canonical message format exchanged with the agent gateway.
|
|
// Aligned with Vercel AI SDK ModelMessage structure.
|
|
type ModelMessage struct {
|
|
Role string `json:"role"`
|
|
Content json.RawMessage `json:"content,omitempty"`
|
|
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
|
ToolCallID string `json:"tool_call_id,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
}
|
|
|
|
// TextContent extracts the plain text from the message content.
|
|
// If content is a string, it returns it directly.
|
|
// If content is an array of parts, it joins all text-type parts.
|
|
func (m ModelMessage) TextContent() string {
|
|
if len(m.Content) == 0 {
|
|
return ""
|
|
}
|
|
var s string
|
|
if err := json.Unmarshal(m.Content, &s); err == nil {
|
|
return s
|
|
}
|
|
var parts []ContentPart
|
|
if err := json.Unmarshal(m.Content, &parts); err == nil {
|
|
texts := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
if strings.TrimSpace(p.Text) != "" {
|
|
texts = append(texts, p.Text)
|
|
}
|
|
}
|
|
return strings.Join(texts, "\n")
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// ContentParts parses the content as an array of ContentPart.
|
|
// Returns nil if the content is a plain string or not parseable.
|
|
func (m ModelMessage) ContentParts() []ContentPart {
|
|
if len(m.Content) == 0 {
|
|
return nil
|
|
}
|
|
var parts []ContentPart
|
|
if err := json.Unmarshal(m.Content, &parts); err != nil {
|
|
return nil
|
|
}
|
|
return parts
|
|
}
|
|
|
|
// HasContent reports whether the message carries non-empty content or tool calls.
|
|
func (m ModelMessage) HasContent() bool {
|
|
if strings.TrimSpace(m.TextContent()) != "" {
|
|
return true
|
|
}
|
|
if len(m.ContentParts()) > 0 {
|
|
return true
|
|
}
|
|
return len(m.ToolCalls) > 0
|
|
}
|
|
|
|
// NewTextContent creates a json.RawMessage from a plain string.
|
|
func NewTextContent(text string) json.RawMessage {
|
|
data, err := json.Marshal(text)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return data
|
|
}
|
|
|
|
// ContentPart represents one element of a multi-part message content.
|
|
type ContentPart struct {
|
|
Type string `json:"type"`
|
|
Text string `json:"text,omitempty"`
|
|
URL string `json:"url,omitempty"`
|
|
Styles []string `json:"styles,omitempty"`
|
|
Language string `json:"language,omitempty"`
|
|
ChannelIdentityID string `json:"channel_identity_id,omitempty"`
|
|
Emoji string `json:"emoji,omitempty"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
}
|
|
|
|
// HasValue reports whether the content part carries a meaningful value.
|
|
func (p ContentPart) HasValue() bool {
|
|
return strings.TrimSpace(p.Text) != "" ||
|
|
strings.TrimSpace(p.URL) != "" ||
|
|
strings.TrimSpace(p.Emoji) != ""
|
|
}
|
|
|
|
// ToolCall represents a function/tool invocation in an assistant message.
|
|
type ToolCall struct {
|
|
ID string `json:"id,omitempty"`
|
|
Type string `json:"type"`
|
|
Function ToolCallFunction `json:"function"`
|
|
}
|
|
|
|
// ToolCallFunction holds the name and serialized arguments of a tool call.
|
|
type ToolCallFunction struct {
|
|
Name string `json:"name"`
|
|
Arguments string `json:"arguments"`
|
|
}
|
|
|
|
// ChatAttachment is a media attachment carried in a chat request.
|
|
type ChatAttachment struct {
|
|
Type string `json:"type"`
|
|
Base64 string `json:"base64,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
URL string `json:"url,omitempty"`
|
|
PlatformKey string `json:"platform_key,omitempty"`
|
|
ContentHash string `json:"content_hash,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
Mime string `json:"mime,omitempty"`
|
|
Size int64 `json:"size,omitempty"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
}
|
|
|
|
// OutboundAssetRef carries an asset reference accumulated during outbound streaming.
|
|
type OutboundAssetRef struct {
|
|
ContentHash string
|
|
Role string
|
|
Ordinal int
|
|
Mime string
|
|
SizeBytes int64
|
|
StorageKey string
|
|
}
|
|
|
|
// ChatRequest is the input for Chat and StreamChat.
|
|
type ChatRequest struct {
|
|
BotID string `json:"-"`
|
|
ChatID string `json:"-"`
|
|
Token string `json:"-"`
|
|
UserID string `json:"-"`
|
|
SourceChannelIdentityID string `json:"-"`
|
|
ContainerID string `json:"-"`
|
|
DisplayName string `json:"-"`
|
|
RouteID string `json:"-"`
|
|
ChatToken string `json:"-"`
|
|
ExternalMessageID string `json:"-"`
|
|
ReplyTarget string `json:"-"`
|
|
ConversationType string `json:"-"`
|
|
ConversationName string `json:"-"`
|
|
UserMessagePersisted bool `json:"-"`
|
|
|
|
// OutboundAssetCollector returns asset refs accumulated during outbound streaming.
|
|
// Set by the inbound channel processor; called by the resolver at persist time.
|
|
OutboundAssetCollector func() []OutboundAssetRef `json:"-"`
|
|
|
|
Query string `json:"query"`
|
|
Model string `json:"model,omitempty"`
|
|
Provider string `json:"provider,omitempty"`
|
|
MaxContextLoadTime int `json:"max_context_load_time,omitempty"`
|
|
Channels []string `json:"channels,omitempty"`
|
|
CurrentChannel string `json:"current_channel,omitempty"`
|
|
Messages []ModelMessage `json:"messages,omitempty"`
|
|
Skills []string `json:"skills,omitempty"`
|
|
Attachments []ChatAttachment `json:"attachments,omitempty"`
|
|
}
|
|
|
|
// ChatResponse is the output of a non-streaming chat call.
|
|
type ChatResponse struct {
|
|
Messages []ModelMessage `json:"messages"`
|
|
Skills []string `json:"skills,omitempty"`
|
|
Model string `json:"model,omitempty"`
|
|
Provider string `json:"provider,omitempty"`
|
|
}
|
|
|
|
// StreamChunk is a raw JSON chunk from the streaming response.
|
|
type StreamChunk = json.RawMessage
|
|
|
|
// AssistantOutput holds extracted assistant content for downstream consumers.
|
|
type AssistantOutput struct {
|
|
Content string
|
|
Parts []ContentPart
|
|
}
|