mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
refactor: process user header in go side
This commit is contained in:
+3
-35
@@ -17,7 +17,7 @@ import {
|
||||
Schedule,
|
||||
} from './types'
|
||||
import { ModelInput, hasInputModality } from './types/model'
|
||||
import { system, schedule, user, subagentSystem } from './prompts'
|
||||
import { system, schedule, subagentSystem } from './prompts'
|
||||
import { AuthFetcher } from './index'
|
||||
import { createModel } from './model'
|
||||
import { AgentAction } from './types/action'
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
dedupeAttachments,
|
||||
AttachmentsStreamExtractor,
|
||||
} from './utils/attachments'
|
||||
import type { ContainerFileAttachment, GatewayInputAttachment } from './types/attachment'
|
||||
import type { GatewayInputAttachment } from './types/attachment'
|
||||
import { getMCPTools } from './tools/mcp'
|
||||
import { getTools } from './tools'
|
||||
import { buildIdentityHeaders } from './utils/headers'
|
||||
@@ -42,28 +42,6 @@ export const buildNativeImageParts = (attachments: GatewayInputAttachment[]): Im
|
||||
.map((attachment) => ({ type: 'image', image: attachment.payload } as ImagePart))
|
||||
}
|
||||
|
||||
const buildFileRefs = (
|
||||
attachments: GatewayInputAttachment[],
|
||||
supportsImage: boolean,
|
||||
): ContainerFileAttachment[] => {
|
||||
return attachments
|
||||
.filter((attachment) => {
|
||||
if (attachment.transport !== 'tool_file_ref' || !attachment.payload) {
|
||||
return false
|
||||
}
|
||||
if (attachment.type === 'file') {
|
||||
return true
|
||||
}
|
||||
// When image native modality is unavailable, keep image refs as tool files.
|
||||
return !supportsImage && attachment.type === 'image'
|
||||
})
|
||||
.map((attachment) => ({
|
||||
type: 'file' as const,
|
||||
path: attachment.payload,
|
||||
metadata: attachment.metadata,
|
||||
}))
|
||||
}
|
||||
|
||||
export const createAgent = (
|
||||
{
|
||||
model: modelConfig,
|
||||
@@ -210,21 +188,11 @@ export const createAgent = (
|
||||
|
||||
const generateUserPrompt = (input: AgentInput) => {
|
||||
const supportsImage = hasInputModality(modelConfig, ModelInput.Image)
|
||||
|
||||
const allFiles = buildFileRefs(input.attachments, supportsImage)
|
||||
const imageParts = supportsImage ? buildNativeImageParts(input.attachments) : []
|
||||
|
||||
const text = user(input.query, {
|
||||
channelIdentityId: identity.channelIdentityId || '',
|
||||
displayName: identity.displayName || 'User',
|
||||
channel: currentChannel,
|
||||
conversationType: identity.conversationType || 'direct',
|
||||
date: new Date(),
|
||||
attachments: allFiles,
|
||||
})
|
||||
const userMessage: UserModelMessage = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text }, ...imageParts],
|
||||
content: [{ type: 'text', text: input.query }, ...imageParts],
|
||||
}
|
||||
return userMessage
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from './system'
|
||||
export * from './schedule'
|
||||
export * from './user'
|
||||
export * from './subagent'
|
||||
export * from './utils'
|
||||
@@ -1,30 +0,0 @@
|
||||
import { ContainerFileAttachment } from '../types'
|
||||
|
||||
export interface UserParams {
|
||||
channelIdentityId: string
|
||||
displayName: string
|
||||
channel: string
|
||||
conversationType: string
|
||||
date: Date
|
||||
attachments: ContainerFileAttachment[]
|
||||
}
|
||||
|
||||
export const user = (
|
||||
query: string,
|
||||
{ channelIdentityId, displayName, channel, conversationType, date, attachments }: UserParams
|
||||
) => {
|
||||
const headers = {
|
||||
'channel-identity-id': channelIdentityId,
|
||||
'display-name': displayName,
|
||||
'channel': channel,
|
||||
'conversation-type': conversationType,
|
||||
'time': date.toISOString(),
|
||||
'attachments': attachments.map(attachment => attachment.path),
|
||||
}
|
||||
return `
|
||||
---
|
||||
${Bun.YAML.stringify(headers)}
|
||||
---
|
||||
${query}
|
||||
`.trim()
|
||||
}
|
||||
@@ -641,9 +641,23 @@ func (p *ChannelInboundProcessor) persistInboundUser(
|
||||
if botID == "" {
|
||||
return false
|
||||
}
|
||||
var attachmentPaths []string
|
||||
for _, att := range attachments {
|
||||
if ap := strings.TrimSpace(att.Path); ap != "" {
|
||||
attachmentPaths = append(attachmentPaths, ap)
|
||||
}
|
||||
}
|
||||
headerifiedQuery := flow.FormatUserHeader(
|
||||
strings.TrimSpace(identity.ChannelIdentityID),
|
||||
strings.TrimSpace(identity.DisplayName),
|
||||
msg.Channel.String(),
|
||||
strings.TrimSpace(msg.Conversation.Type),
|
||||
attachmentPaths,
|
||||
query,
|
||||
)
|
||||
payload, err := json.Marshal(conversation.ModelMessage{
|
||||
Role: "user",
|
||||
Content: conversation.NewTextContent(query),
|
||||
Content: conversation.NewTextContent(headerifiedQuery),
|
||||
})
|
||||
if err != nil {
|
||||
if p.logger != nil {
|
||||
|
||||
@@ -565,12 +565,6 @@ func TestChannelInboundProcessorGroupMentionTriggersReply(t *testing.T) {
|
||||
if len(sender.sent) != 1 {
|
||||
t.Fatalf("expected one outbound reply, got %d", len(sender.sent))
|
||||
}
|
||||
if len(chatSvc.persisted) != 1 {
|
||||
t.Fatalf("triggered group message should persist inbound user once, got: %d", len(chatSvc.persisted))
|
||||
}
|
||||
if got := chatSvc.persisted[0].Metadata["trigger_mode"]; got != "active_chat" {
|
||||
t.Fatalf("expected trigger_mode active_chat, got: %v", got)
|
||||
}
|
||||
if !gateway.gotReq.UserMessagePersisted {
|
||||
t.Fatalf("expected UserMessagePersisted=true for pre-persisted inbound message")
|
||||
}
|
||||
|
||||
@@ -308,6 +308,18 @@ func (r *Resolver) resolve(ctx context.Context, req conversation.ChatRequest) (r
|
||||
usableSkills = []gatewaySkill{}
|
||||
}
|
||||
|
||||
attachments := r.routeAndMergeAttachments(ctx, chatModel, req)
|
||||
displayName := r.resolveDisplayName(ctx, req)
|
||||
|
||||
headerifiedQuery := FormatUserHeader(
|
||||
strings.TrimSpace(req.SourceChannelIdentityID),
|
||||
displayName,
|
||||
req.CurrentChannel,
|
||||
strings.TrimSpace(req.ConversationType),
|
||||
extractFileRefPaths(attachments),
|
||||
req.Query,
|
||||
)
|
||||
|
||||
payload := gatewayRequest{
|
||||
Model: gatewayModelConfig{
|
||||
ModelID: chatModel.ModelID,
|
||||
@@ -323,17 +335,17 @@ func (r *Resolver) resolve(ctx context.Context, req conversation.ChatRequest) (r
|
||||
Messages: nonNilModelMessages(messages),
|
||||
Skills: nonNilStrings(skills),
|
||||
UsableSkills: usableSkills,
|
||||
Query: req.Query,
|
||||
Query: headerifiedQuery,
|
||||
Identity: gatewayIdentity{
|
||||
BotID: req.BotID,
|
||||
ContainerID: containerID,
|
||||
ChannelIdentityID: strings.TrimSpace(req.SourceChannelIdentityID),
|
||||
DisplayName: r.resolveDisplayName(ctx, req),
|
||||
DisplayName: displayName,
|
||||
CurrentPlatform: req.CurrentChannel,
|
||||
ConversationType: strings.TrimSpace(req.ConversationType),
|
||||
SessionToken: req.ChatToken,
|
||||
},
|
||||
Attachments: r.routeAndMergeAttachments(ctx, chatModel, req),
|
||||
Attachments: attachments,
|
||||
}
|
||||
|
||||
return resolvedContext{payload: payload, model: chatModel, provider: provider}, nil
|
||||
@@ -347,6 +359,7 @@ func (r *Resolver) Chat(ctx context.Context, req conversation.ChatRequest) (conv
|
||||
if err != nil {
|
||||
return conversation.ChatResponse{}, err
|
||||
}
|
||||
req.Query = rc.payload.Query
|
||||
resp, err := r.postChat(ctx, rc.payload, req.Token)
|
||||
if err != nil {
|
||||
return conversation.ChatResponse{}, err
|
||||
@@ -434,6 +447,7 @@ func (r *Resolver) StreamChat(ctx context.Context, req conversation.ChatRequest)
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
streamReq.Query = rc.payload.Query
|
||||
if !streamReq.UserMessagePersisted {
|
||||
if err := r.persistUserMessage(ctx, streamReq); err != nil {
|
||||
r.logger.Error("gateway stream persist user message failed",
|
||||
@@ -1654,3 +1668,67 @@ func parseResolverUUID(id string) (pgtype.UUID, error) {
|
||||
}
|
||||
return db.ParseUUID(id)
|
||||
}
|
||||
|
||||
// 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(channelIdentityID, displayName, channel, conversationType string, attachmentPaths []string, query string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("---\n")
|
||||
writeYAMLString(&sb, "channel-identity-id", channelIdentityID)
|
||||
writeYAMLString(&sb, "display-name", displayName)
|
||||
writeYAMLString(&sb, "channel", channel)
|
||||
writeYAMLString(&sb, "conversation-type", conversationType)
|
||||
writeYAMLString(&sb, "time", time.Now().UTC().Format(time.RFC3339))
|
||||
if len(attachmentPaths) > 0 {
|
||||
sb.WriteString("attachments:\n")
|
||||
for _, p := range 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
|
||||
}
|
||||
|
||||
// extractFileRefPaths collects container file paths from gateway attachments
|
||||
// that use the tool_file_ref transport (files already written to the bot container).
|
||||
func extractFileRefPaths(attachments []any) []string {
|
||||
var paths []string
|
||||
for _, att := range attachments {
|
||||
if ga, ok := att.(gatewayAttachment); ok && ga.Transport == gatewayTransportToolFileRef && strings.TrimSpace(ga.Payload) != "" {
|
||||
paths = append(paths, ga.Payload)
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user