mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
06e8619a37
Align channel identity and bind flow across backend and app-facing layers, including generated swagger artifacts and package lock updates while excluding docs content changes.
275 lines
9.5 KiB
Go
275 lines
9.5 KiB
Go
// Package chat orchestrates conversations with the agent gateway, including
|
|
// synchronous and streaming chat, scheduled triggers, messages, and memory storage.
|
|
package chat
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Chat kind constants.
|
|
const (
|
|
KindDirect = "direct"
|
|
KindGroup = "group"
|
|
KindThread = "thread"
|
|
)
|
|
|
|
// Participant role constants.
|
|
const (
|
|
RoleOwner = "owner"
|
|
RoleAdmin = "admin"
|
|
RoleMember = "member"
|
|
)
|
|
|
|
// Chat list access mode constants.
|
|
const (
|
|
AccessModeParticipant = "participant"
|
|
AccessModeChannelIdentityObserved = "channel_identity_observed"
|
|
)
|
|
|
|
// Chat is the first-class conversation container.
|
|
type Chat 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"`
|
|
}
|
|
|
|
// ChatListItem is a chat entry with access context for list rendering.
|
|
type ChatListItem 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"`
|
|
}
|
|
|
|
// ChatReadAccess is the resolved access context for reading chat content.
|
|
type ChatReadAccess 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"`
|
|
EnableChatMemory bool `json:"enable_chat_memory"`
|
|
EnablePrivateMemory bool `json:"enable_private_memory"`
|
|
EnablePublicMemory bool `json:"enable_public_memory"`
|
|
ModelID string `json:"model_id,omitempty"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
}
|
|
|
|
// Route maps external channel conversations to a chat.
|
|
type Route struct {
|
|
ID string `json:"id"`
|
|
ChatID string `json:"chat_id"`
|
|
BotID string `json:"bot_id"`
|
|
Platform string `json:"platform"`
|
|
ChannelConfigID string `json:"channel_config_id,omitempty"`
|
|
ConversationID string `json:"conversation_id"`
|
|
ThreadID string `json:"thread_id,omitempty"`
|
|
ReplyTarget string `json:"reply_target,omitempty"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// Message represents a single persisted chat message.
|
|
type Message struct {
|
|
ID string `json:"id"`
|
|
ChatID string `json:"chat_id"`
|
|
BotID string `json:"bot_id"`
|
|
RouteID string `json:"route_id,omitempty"`
|
|
SenderChannelIdentityID string `json:"sender_channel_identity_id,omitempty"`
|
|
SenderUserID string `json:"sender_user_id,omitempty"`
|
|
Platform string `json:"platform,omitempty"`
|
|
ExternalMessageID string `json:"external_message_id,omitempty"`
|
|
Role string `json:"role"`
|
|
Content json.RawMessage `json:"content"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// CreateChatRequest is the input for creating a chat.
|
|
type CreateChatRequest 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 {
|
|
EnableChatMemory *bool `json:"enable_chat_memory,omitempty"`
|
|
EnablePrivateMemory *bool `json:"enable_private_memory,omitempty"`
|
|
EnablePublicMemory *bool `json:"enable_public_memory,omitempty"`
|
|
ModelID *string `json:"model_id,omitempty"`
|
|
}
|
|
|
|
// ResolveChatResult is returned by ResolveChat.
|
|
type ResolveChatResult struct {
|
|
ChatID string
|
|
RouteID string
|
|
Created bool
|
|
}
|
|
|
|
// 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, _ := json.Marshal(text)
|
|
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"`
|
|
}
|
|
|
|
// ChatRequest is the input for Chat and StreamChat.
|
|
type ChatRequest struct {
|
|
BotID string `json:"-"`
|
|
ChatID string `json:"-"`
|
|
Token string `json:"-"`
|
|
ChannelIdentityID string `json:"-"`
|
|
ContainerID string `json:"-"`
|
|
DisplayName string `json:"-"`
|
|
RouteID string `json:"-"`
|
|
ChatToken string `json:"-"`
|
|
ExternalMessageID string `json:"-"`
|
|
|
|
Query string `json:"query"`
|
|
Model string `json:"model,omitempty"`
|
|
Provider string `json:"provider,omitempty"`
|
|
MaxContextLoadTime int `json:"max_context_load_time,omitempty"`
|
|
Language string `json:"language,omitempty"`
|
|
Channels []string `json:"channels,omitempty"`
|
|
CurrentChannel string `json:"current_channel,omitempty"`
|
|
Messages []ModelMessage `json:"messages,omitempty"`
|
|
Skills []string `json:"skills,omitempty"`
|
|
AllowedActions []string `json:"allowed_actions,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
|
|
}
|