feat: sync AI compose and ChatBot features

This commit is contained in:
A 2026-07-03 19:43:20 +08:00
parent 35e5d38f4d
commit b7269b135f
75 changed files with 5426 additions and 123 deletions

View file

@ -0,0 +1,101 @@
package messages
import (
"context"
"strings"
"unicode/utf8"
"telesrv/internal/domain"
)
type BusinessAITextGenerator interface {
GenerateText(ctx context.Context, req domain.AITextGenerationRequest) (domain.AIComposeText, error)
}
type AIBusinessAutomationProvider struct {
generator BusinessAITextGenerator
}
func NewAIBusinessAutomationProvider(generator BusinessAITextGenerator) AIBusinessAutomationProvider {
return AIBusinessAutomationProvider{generator: generator}
}
func (p AIBusinessAutomationProvider) BusinessAutomationReplies(ctx context.Context, input BusinessAutomationReplyInput) ([]domain.QuickReplyMessage, error) {
if p.generator == nil {
return nil, nil
}
body := strings.TrimSpace(input.TriggerMessage.Body)
if body == "" {
return nil, nil
}
out, err := p.generator.GenerateText(ctx, domain.AITextGenerationRequest{
UserID: input.OwnerUserID,
Text: domain.AIComposeText{
Text: input.TriggerMessage.Body,
Entities: append([]domain.MessageEntity(nil), input.TriggerMessage.Entities...),
},
Instruction: businessAIReplyInstruction(input),
})
if err != nil {
return nil, err
}
text := strings.TrimSpace(out.Text)
if text == "" || utf8.RuneCountInString(text) > domain.MaxMessageTextLength || len(out.Entities) > domain.MaxMessageEntityCount {
return nil, nil
}
return []domain.QuickReplyMessage{{
ID: 1,
Date: input.Now,
Message: text,
Entities: append([]domain.MessageEntity(nil), out.Entities...),
}}, nil
}
func businessAIReplyInstruction(input BusinessAutomationReplyInput) string {
parts := []string{
"Write one brief, helpful chat reply from the business owner to the customer.",
"Return only the message text, without explanations, markdown fences, labels, quotes, or signatures.",
"Do not claim to be an AI and do not mention internal automation rules.",
}
switch input.Kind {
case domain.BusinessAutomationGreeting:
parts = append(parts, "Context: this is a greeting or first response for the conversation.")
case domain.BusinessAutomationAway:
parts = append(parts, "Context: the business owner may be away; acknowledge the customer naturally without promising exact availability.")
case domain.BusinessAutomationAI:
parts = append(parts, "Context: this is a connected business bot reply on behalf of the owner.")
}
if len(input.Templates) > 0 {
parts = append(parts, "Owner quick reply templates may be used as style or policy hints:")
count := 0
for _, tmpl := range input.Templates {
text := strings.TrimSpace(tmpl.Message)
if text == "" {
continue
}
parts = append(parts, "- "+trimInstructionLine(text, 240))
count++
if count >= 3 {
break
}
}
}
return strings.Join(parts, "\n")
}
func trimInstructionLine(text string, maxRunes int) string {
text = strings.Join(strings.Fields(text), " ")
if maxRunes <= 0 || utf8.RuneCountInString(text) <= maxRunes {
return text
}
var b strings.Builder
count := 0
for _, r := range text {
if count >= maxRunes {
break
}
b.WriteRune(r)
count++
}
return b.String()
}