feat(channel): pass conversation type through to agent gateway and persist in route

Propagate conversation type (direct/group/thread) from channel adapters
all the way to the agent prompt. Store conversation_type on bot_channel_routes
so the bot knows whether a message originates from a p2p chat, group, or thread.

Schema changes are folded into the 0001 init migration (destructive update).
This commit is contained in:
BBQ
2026-02-13 05:10:35 +08:00
parent 0406f42e86
commit faaadf14c5
18 changed files with 173 additions and 153 deletions
+1
View File
@@ -279,6 +279,7 @@ func (p *ChannelInboundProcessor) HandleInbound(ctx context.Context, cfg channel
RouteID: resolved.RouteID,
ChatToken: chatToken,
ExternalMessageID: sourceMessageID,
ConversationType: msg.Conversation.Type,
Query: text,
CurrentChannel: msg.Channel.String(),
Channels: []string{msg.Channel.String()},
+42 -71
View File
@@ -63,14 +63,15 @@ func (s *DBService) Create(ctx context.Context, input CreateInput) (Route, error
}
row, err := s.queries.CreateChatRoute(ctx, sqlc.CreateChatRouteParams{
ChatID: pgConversationID,
BotID: pgBotID,
Platform: input.Platform,
ChannelConfigID: pgConfigID,
ConversationID: input.ConversationID,
ThreadID: toPgText(input.ThreadID),
ReplyTarget: toPgText(input.ReplyTarget),
Metadata: metadata,
ChatID: pgConversationID,
BotID: pgBotID,
Platform: input.Platform,
ChannelConfigID: pgConfigID,
ConversationID: input.ConversationID,
ThreadID: toPgText(input.ThreadID),
ConversationType: toPgText(input.ConversationType),
ReplyTarget: toPgText(input.ReplyTarget),
Metadata: metadata,
})
if err != nil {
return Route{}, fmt.Errorf("create route: %w", err)
@@ -208,13 +209,14 @@ func (s *DBService) ResolveConversation(ctx context.Context, input ResolveInput)
}
newRoute, err := s.Create(ctx, CreateInput{
ChatID: createdConversation.ID,
BotID: input.BotID,
Platform: input.Platform,
ChannelConfigID: input.ChannelConfigID,
ConversationID: input.ConversationID,
ThreadID: input.ThreadID,
ReplyTarget: input.ReplyTarget,
ChatID: createdConversation.ID,
BotID: input.BotID,
Platform: input.Platform,
ChannelConfigID: input.ChannelConfigID,
ConversationID: input.ConversationID,
ThreadID: input.ThreadID,
ConversationType: input.ConversationType,
ReplyTarget: input.ReplyTarget,
})
if err != nil {
return ResolveConversationResult{}, fmt.Errorf("create route: %w", err)
@@ -260,81 +262,50 @@ func (s *DBService) resolveConversationCreatorChannelIdentityID(ctx context.Cont
func toRouteFromCreate(row sqlc.CreateChatRouteRow) Route {
return toRouteFields(
row.ID,
row.ChatID,
row.BotID,
row.Platform,
row.ChannelConfigID,
row.ConversationID,
row.ThreadID,
row.ReplyTarget,
row.Metadata,
row.CreatedAt,
row.UpdatedAt,
row.ID, row.ChatID, row.BotID, row.Platform, row.ChannelConfigID,
row.ConversationID, row.ThreadID, row.ConversationType, row.ReplyTarget,
row.Metadata, row.CreatedAt, row.UpdatedAt,
)
}
func toRouteFromFind(row sqlc.FindChatRouteRow) Route {
return toRouteFields(
row.ID,
row.ChatID,
row.BotID,
row.Platform,
row.ChannelConfigID,
row.ConversationID,
row.ThreadID,
row.ReplyTarget,
row.Metadata,
row.CreatedAt,
row.UpdatedAt,
row.ID, row.ChatID, row.BotID, row.Platform, row.ChannelConfigID,
row.ConversationID, row.ThreadID, row.ConversationType, row.ReplyTarget,
row.Metadata, row.CreatedAt, row.UpdatedAt,
)
}
func toRouteFromGet(row sqlc.GetChatRouteByIDRow) Route {
return toRouteFields(
row.ID,
row.ChatID,
row.BotID,
row.Platform,
row.ChannelConfigID,
row.ConversationID,
row.ThreadID,
row.ReplyTarget,
row.Metadata,
row.CreatedAt,
row.UpdatedAt,
row.ID, row.ChatID, row.BotID, row.Platform, row.ChannelConfigID,
row.ConversationID, row.ThreadID, row.ConversationType, row.ReplyTarget,
row.Metadata, row.CreatedAt, row.UpdatedAt,
)
}
func toRouteFromList(row sqlc.ListChatRoutesRow) Route {
return toRouteFields(
row.ID,
row.ChatID,
row.BotID,
row.Platform,
row.ChannelConfigID,
row.ConversationID,
row.ThreadID,
row.ReplyTarget,
row.Metadata,
row.CreatedAt,
row.UpdatedAt,
row.ID, row.ChatID, row.BotID, row.Platform, row.ChannelConfigID,
row.ConversationID, row.ThreadID, row.ConversationType, row.ReplyTarget,
row.Metadata, row.CreatedAt, row.UpdatedAt,
)
}
func toRouteFields(id, conversationID, botID pgtype.UUID, platform string, channelConfigID pgtype.UUID, externalConversationID string, threadID, replyTarget pgtype.Text, metadata []byte, createdAt, updatedAt pgtype.Timestamptz) Route {
func toRouteFields(id, conversationID, botID pgtype.UUID, platform string, channelConfigID pgtype.UUID, externalConversationID string, threadID, conversationType, replyTarget pgtype.Text, metadata []byte, createdAt, updatedAt pgtype.Timestamptz) Route {
return Route{
ID: id.String(),
ChatID: conversationID.String(),
BotID: botID.String(),
Platform: platform,
ChannelConfigID: channelConfigID.String(),
ConversationID: externalConversationID,
ThreadID: dbpkg.TextToString(threadID),
ReplyTarget: dbpkg.TextToString(replyTarget),
Metadata: parseJSONMap(metadata),
CreatedAt: createdAt.Time,
UpdatedAt: updatedAt.Time,
ID: id.String(),
ChatID: conversationID.String(),
BotID: botID.String(),
Platform: platform,
ChannelConfigID: channelConfigID.String(),
ConversationID: externalConversationID,
ThreadID: dbpkg.TextToString(threadID),
ConversationType: dbpkg.TextToString(conversationType),
ReplyTarget: dbpkg.TextToString(replyTarget),
Metadata: parseJSONMap(metadata),
CreatedAt: createdAt.Time,
UpdatedAt: updatedAt.Time,
}
}
+21 -19
View File
@@ -7,17 +7,18 @@ import (
// Route maps external channel conversations to an internal conversation.
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"`
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"`
ConversationType string `json:"conversation_type,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"`
}
// ResolveConversationResult is returned by ResolveConversation.
@@ -29,14 +30,15 @@ type ResolveConversationResult struct {
// CreateInput is the input for creating a route.
type CreateInput struct {
ChatID string
BotID string
Platform string
ChannelConfigID string
ConversationID string
ThreadID string
ReplyTarget string
Metadata map[string]any
ChatID string
BotID string
Platform string
ChannelConfigID string
ConversationID string
ThreadID string
ConversationType string
ReplyTarget string
Metadata map[string]any
}
// ResolveInput is the input for route-to-conversation resolution.
+2
View File
@@ -122,6 +122,7 @@ type gatewayIdentity struct {
ChannelIdentityID string `json:"channelIdentityId"`
DisplayName string `json:"displayName"`
CurrentPlatform string `json:"currentPlatform,omitempty"`
ConversationType string `json:"conversationType,omitempty"`
SessionToken string `json:"sessionToken,omitempty"`
}
@@ -270,6 +271,7 @@ func (r *Resolver) resolve(ctx context.Context, req conversation.ChatRequest) (r
ChannelIdentityID: strings.TrimSpace(req.SourceChannelIdentityID),
DisplayName: r.resolveDisplayName(ctx, req),
CurrentPlatform: req.CurrentChannel,
ConversationType: strings.TrimSpace(req.ConversationType),
SessionToken: req.ChatToken,
},
Attachments: []any{},
+1
View File
@@ -203,6 +203,7 @@ type ChatRequest struct {
RouteID string `json:"-"`
ChatToken string `json:"-"`
ExternalMessageID string `json:"-"`
ConversationType string `json:"-"`
UserMessagePersisted bool `json:"-"`
Query string `json:"query"`
+70 -55
View File
@@ -13,7 +13,7 @@ import (
const createChatRoute = `-- name: CreateChatRoute :one
INSERT INTO bot_channel_routes (
bot_id, channel_type, channel_config_id, external_conversation_id, external_thread_id, default_reply_target, metadata
bot_id, channel_type, channel_config_id, external_conversation_id, external_thread_id, conversation_type, default_reply_target, metadata
)
VALUES (
$1,
@@ -22,16 +22,18 @@ VALUES (
$4,
$5::text,
$6::text,
$7
$7::text,
$8
)
RETURNING
id,
$8::uuid AS chat_id,
$9::uuid AS chat_id,
bot_id,
channel_type AS platform,
channel_config_id,
external_conversation_id AS conversation_id,
external_thread_id AS thread_id,
conversation_type,
default_reply_target AS reply_target,
metadata,
created_at,
@@ -39,28 +41,30 @@ RETURNING
`
type CreateChatRouteParams struct {
BotID pgtype.UUID `json:"bot_id"`
Platform string `json:"platform"`
ChannelConfigID pgtype.UUID `json:"channel_config_id"`
ConversationID string `json:"conversation_id"`
ThreadID pgtype.Text `json:"thread_id"`
ReplyTarget pgtype.Text `json:"reply_target"`
Metadata []byte `json:"metadata"`
ChatID pgtype.UUID `json:"chat_id"`
BotID pgtype.UUID `json:"bot_id"`
Platform string `json:"platform"`
ChannelConfigID pgtype.UUID `json:"channel_config_id"`
ConversationID string `json:"conversation_id"`
ThreadID pgtype.Text `json:"thread_id"`
ConversationType pgtype.Text `json:"conversation_type"`
ReplyTarget pgtype.Text `json:"reply_target"`
Metadata []byte `json:"metadata"`
ChatID pgtype.UUID `json:"chat_id"`
}
type CreateChatRouteRow struct {
ID pgtype.UUID `json:"id"`
ChatID pgtype.UUID `json:"chat_id"`
BotID pgtype.UUID `json:"bot_id"`
Platform string `json:"platform"`
ChannelConfigID pgtype.UUID `json:"channel_config_id"`
ConversationID string `json:"conversation_id"`
ThreadID pgtype.Text `json:"thread_id"`
ReplyTarget pgtype.Text `json:"reply_target"`
Metadata []byte `json:"metadata"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ID pgtype.UUID `json:"id"`
ChatID pgtype.UUID `json:"chat_id"`
BotID pgtype.UUID `json:"bot_id"`
Platform string `json:"platform"`
ChannelConfigID pgtype.UUID `json:"channel_config_id"`
ConversationID string `json:"conversation_id"`
ThreadID pgtype.Text `json:"thread_id"`
ConversationType pgtype.Text `json:"conversation_type"`
ReplyTarget pgtype.Text `json:"reply_target"`
Metadata []byte `json:"metadata"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) CreateChatRoute(ctx context.Context, arg CreateChatRouteParams) (CreateChatRouteRow, error) {
@@ -70,6 +74,7 @@ func (q *Queries) CreateChatRoute(ctx context.Context, arg CreateChatRouteParams
arg.ChannelConfigID,
arg.ConversationID,
arg.ThreadID,
arg.ConversationType,
arg.ReplyTarget,
arg.Metadata,
arg.ChatID,
@@ -83,6 +88,7 @@ func (q *Queries) CreateChatRoute(ctx context.Context, arg CreateChatRouteParams
&i.ChannelConfigID,
&i.ConversationID,
&i.ThreadID,
&i.ConversationType,
&i.ReplyTarget,
&i.Metadata,
&i.CreatedAt,
@@ -110,6 +116,7 @@ SELECT
channel_config_id,
external_conversation_id AS conversation_id,
external_thread_id AS thread_id,
conversation_type,
default_reply_target AS reply_target,
metadata,
created_at,
@@ -130,17 +137,18 @@ type FindChatRouteParams struct {
}
type FindChatRouteRow struct {
ID pgtype.UUID `json:"id"`
ChatID pgtype.UUID `json:"chat_id"`
BotID pgtype.UUID `json:"bot_id"`
Platform string `json:"platform"`
ChannelConfigID pgtype.UUID `json:"channel_config_id"`
ConversationID string `json:"conversation_id"`
ThreadID pgtype.Text `json:"thread_id"`
ReplyTarget pgtype.Text `json:"reply_target"`
Metadata []byte `json:"metadata"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ID pgtype.UUID `json:"id"`
ChatID pgtype.UUID `json:"chat_id"`
BotID pgtype.UUID `json:"bot_id"`
Platform string `json:"platform"`
ChannelConfigID pgtype.UUID `json:"channel_config_id"`
ConversationID string `json:"conversation_id"`
ThreadID pgtype.Text `json:"thread_id"`
ConversationType pgtype.Text `json:"conversation_type"`
ReplyTarget pgtype.Text `json:"reply_target"`
Metadata []byte `json:"metadata"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) FindChatRoute(ctx context.Context, arg FindChatRouteParams) (FindChatRouteRow, error) {
@@ -159,6 +167,7 @@ func (q *Queries) FindChatRoute(ctx context.Context, arg FindChatRouteParams) (F
&i.ChannelConfigID,
&i.ConversationID,
&i.ThreadID,
&i.ConversationType,
&i.ReplyTarget,
&i.Metadata,
&i.CreatedAt,
@@ -176,6 +185,7 @@ SELECT
channel_config_id,
external_conversation_id AS conversation_id,
external_thread_id AS thread_id,
conversation_type,
default_reply_target AS reply_target,
metadata,
created_at,
@@ -185,17 +195,18 @@ WHERE id = $1
`
type GetChatRouteByIDRow struct {
ID pgtype.UUID `json:"id"`
ChatID pgtype.UUID `json:"chat_id"`
BotID pgtype.UUID `json:"bot_id"`
Platform string `json:"platform"`
ChannelConfigID pgtype.UUID `json:"channel_config_id"`
ConversationID string `json:"conversation_id"`
ThreadID pgtype.Text `json:"thread_id"`
ReplyTarget pgtype.Text `json:"reply_target"`
Metadata []byte `json:"metadata"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ID pgtype.UUID `json:"id"`
ChatID pgtype.UUID `json:"chat_id"`
BotID pgtype.UUID `json:"bot_id"`
Platform string `json:"platform"`
ChannelConfigID pgtype.UUID `json:"channel_config_id"`
ConversationID string `json:"conversation_id"`
ThreadID pgtype.Text `json:"thread_id"`
ConversationType pgtype.Text `json:"conversation_type"`
ReplyTarget pgtype.Text `json:"reply_target"`
Metadata []byte `json:"metadata"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) GetChatRouteByID(ctx context.Context, id pgtype.UUID) (GetChatRouteByIDRow, error) {
@@ -209,6 +220,7 @@ func (q *Queries) GetChatRouteByID(ctx context.Context, id pgtype.UUID) (GetChat
&i.ChannelConfigID,
&i.ConversationID,
&i.ThreadID,
&i.ConversationType,
&i.ReplyTarget,
&i.Metadata,
&i.CreatedAt,
@@ -226,6 +238,7 @@ SELECT
channel_config_id,
external_conversation_id AS conversation_id,
external_thread_id AS thread_id,
conversation_type,
default_reply_target AS reply_target,
metadata,
created_at,
@@ -236,17 +249,18 @@ ORDER BY created_at ASC
`
type ListChatRoutesRow struct {
ID pgtype.UUID `json:"id"`
ChatID pgtype.UUID `json:"chat_id"`
BotID pgtype.UUID `json:"bot_id"`
Platform string `json:"platform"`
ChannelConfigID pgtype.UUID `json:"channel_config_id"`
ConversationID string `json:"conversation_id"`
ThreadID pgtype.Text `json:"thread_id"`
ReplyTarget pgtype.Text `json:"reply_target"`
Metadata []byte `json:"metadata"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ID pgtype.UUID `json:"id"`
ChatID pgtype.UUID `json:"chat_id"`
BotID pgtype.UUID `json:"bot_id"`
Platform string `json:"platform"`
ChannelConfigID pgtype.UUID `json:"channel_config_id"`
ConversationID string `json:"conversation_id"`
ThreadID pgtype.Text `json:"thread_id"`
ConversationType pgtype.Text `json:"conversation_type"`
ReplyTarget pgtype.Text `json:"reply_target"`
Metadata []byte `json:"metadata"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) ListChatRoutes(ctx context.Context, chatID pgtype.UUID) ([]ListChatRoutesRow, error) {
@@ -266,6 +280,7 @@ func (q *Queries) ListChatRoutes(ctx context.Context, chatID pgtype.UUID) ([]Lis
&i.ChannelConfigID,
&i.ConversationID,
&i.ThreadID,
&i.ConversationType,
&i.ReplyTarget,
&i.Metadata,
&i.CreatedAt,
+1
View File
@@ -49,6 +49,7 @@ type BotChannelRoute struct {
ChannelConfigID pgtype.UUID `json:"channel_config_id"`
ExternalConversationID string `json:"external_conversation_id"`
ExternalThreadID pgtype.Text `json:"external_thread_id"`
ConversationType pgtype.Text `json:"conversation_type"`
DefaultReplyTarget pgtype.Text `json:"default_reply_target"`
Metadata []byte `json:"metadata"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
+12 -6
View File
@@ -98,6 +98,9 @@ func (h *MessageHandler) SendMessage(c echo.Context) error {
if strings.TrimSpace(req.CurrentChannel) == "" {
req.CurrentChannel = "web"
}
if strings.TrimSpace(req.ConversationType) == "" {
req.ConversationType = "direct"
}
if len(req.Channels) == 0 {
req.Channels = []string{req.CurrentChannel}
}
@@ -145,6 +148,9 @@ func (h *MessageHandler) StreamMessage(c echo.Context) error {
if strings.TrimSpace(req.CurrentChannel) == "" {
req.CurrentChannel = "web"
}
if strings.TrimSpace(req.ConversationType) == "" {
req.ConversationType = "direct"
}
if len(req.Channels) == 0 {
req.Channels = []string{req.CurrentChannel}
}
@@ -198,12 +204,12 @@ func (h *MessageHandler) StreamMessage(c echo.Context) error {
h.logger.Error("conversation stream failed", slog.Any("error", err))
if processingState == "started" {
processingState = "failed"
if writeErr := writeSSEJSON(writer, flusher, map[string]string{
"type": "processing_failed",
"error": err.Error(),
}); writeErr != nil {
h.logger.Warn("write SSE processing_failed event failed", slog.Any("error", writeErr))
}
if writeErr := writeSSEJSON(writer, flusher, map[string]string{
"type": "processing_failed",
"error": err.Error(),
}); writeErr != nil {
h.logger.Warn("write SSE processing_failed event failed", slog.Any("error", writeErr))
}
}
errData := map[string]string{
"type": "error",