feat(moderation): add Tencent TMS as fallback for AI content moderation
Add Tencent Cloud Text Moderation System (TMS) as a fallback moderation service when OpenAI is unavailable or returns an error. The service now checks OpenAI first, and falls back to Tencent TMS if it fails, providing better reliability for content moderation. Features: - New Tencent TMS configuration in config.yaml with environment variable overrides - New tencent package in internal/pkg/tencent/ with TMS client implementation - PostAIService now uses dual moderation with OpenAI primary and Tencent fallback - Strict mode configuration passed to PostAIService for consistent behavior BREAKING CHANGE: NewPostAIService now requires tencentClient and strictMode parameters - update wire configuration accordingly.
This commit is contained in:
@@ -2,9 +2,11 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"carrot_bbs/internal/pkg/openai"
|
||||
"carrot_bbs/internal/pkg/tencent"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -170,43 +172,66 @@ func (e *BioModerationReviewError) IsReview() bool {
|
||||
}
|
||||
|
||||
type PostAIService struct {
|
||||
openAIClient openai.Client
|
||||
openAIClient openai.Client
|
||||
tencentClient tencent.Client
|
||||
strictMode bool
|
||||
}
|
||||
|
||||
func NewPostAIService(openAIClient openai.Client) *PostAIService {
|
||||
func NewPostAIService(openAIClient openai.Client, tencentClient tencent.Client, strictMode bool) *PostAIService {
|
||||
return &PostAIService{
|
||||
openAIClient: openAIClient,
|
||||
openAIClient: openAIClient,
|
||||
tencentClient: tencentClient,
|
||||
strictMode: strictMode,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PostAIService) IsEnabled() bool {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
return (s.openAIClient != nil && s.openAIClient.IsEnabled()) ||
|
||||
(s.tencentClient != nil && s.tencentClient.IsEnabled())
|
||||
}
|
||||
|
||||
func (s *PostAIService) isOpenAIEnabled() bool {
|
||||
return s != nil && s.openAIClient != nil && s.openAIClient.IsEnabled()
|
||||
}
|
||||
|
||||
func (s *PostAIService) isTencentEnabled() bool {
|
||||
return s != nil && s.tencentClient != nil && s.tencentClient.IsEnabled()
|
||||
}
|
||||
|
||||
func (s *PostAIService) ModeratePost(ctx context.Context, title, content string, images []string) error {
|
||||
if !s.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := s.openAIClient.ModeratePost(ctx, title, content, images)
|
||||
if err != nil {
|
||||
if s.openAIClient.Config().StrictModeration {
|
||||
return err
|
||||
if s.isOpenAIEnabled() {
|
||||
resp, err := s.openAIClient.ModeratePost(ctx, title, content, images)
|
||||
if err != nil {
|
||||
zap.L().Warn("AI post moderation failed, trying Tencent fallback",
|
||||
zap.Error(err),
|
||||
)
|
||||
return s.moderateTextWithTencent(ctx, title+" "+content,
|
||||
func(reason string) error { return &PostModerationRejectedError{Reason: reason} },
|
||||
func(reason string) error { return &PostModerationReviewError{Reason: reason} },
|
||||
)
|
||||
}
|
||||
|
||||
switch resp.Result {
|
||||
case openai.ModerationResultBlock:
|
||||
return &PostModerationRejectedError{Reason: resp.Reason}
|
||||
case openai.ModerationResultReview:
|
||||
return &PostModerationReviewError{Reason: resp.Reason}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
zap.L().Warn("AI moderation failed, fallback allow",
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
switch resp.Result {
|
||||
case openai.ModerationResultBlock:
|
||||
return &PostModerationRejectedError{Reason: resp.Reason}
|
||||
case openai.ModerationResultReview:
|
||||
return &PostModerationReviewError{Reason: resp.Reason}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return s.moderateTextWithTencent(ctx, title+" "+content,
|
||||
func(reason string) error { return &PostModerationRejectedError{Reason: reason} },
|
||||
func(reason string) error { return &PostModerationReviewError{Reason: reason} },
|
||||
)
|
||||
}
|
||||
|
||||
func (s *PostAIService) ModerateComment(ctx context.Context, content string, images []string) error {
|
||||
@@ -214,35 +239,42 @@ func (s *PostAIService) ModerateComment(ctx context.Context, content string, ima
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := s.openAIClient.ModerateComment(ctx, content, images)
|
||||
if err != nil {
|
||||
if s.openAIClient.Config().StrictModeration {
|
||||
return err
|
||||
if s.isOpenAIEnabled() {
|
||||
resp, err := s.openAIClient.ModerateComment(ctx, content, images)
|
||||
if err != nil {
|
||||
zap.L().Warn("AI comment moderation failed, trying Tencent fallback",
|
||||
zap.Error(err),
|
||||
)
|
||||
return s.moderateTextWithTencent(ctx, content,
|
||||
func(reason string) error { return &CommentModerationRejectedError{Reason: reason} },
|
||||
func(reason string) error { return &CommentModerationReviewError{Reason: reason} },
|
||||
)
|
||||
}
|
||||
|
||||
switch resp.Result {
|
||||
case openai.ModerationResultBlock:
|
||||
return &CommentModerationRejectedError{Reason: resp.Reason}
|
||||
case openai.ModerationResultReview:
|
||||
return &CommentModerationReviewError{Reason: resp.Reason}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
zap.L().Warn("AI comment moderation failed, fallback allow",
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
switch resp.Result {
|
||||
case openai.ModerationResultBlock:
|
||||
return &CommentModerationRejectedError{Reason: resp.Reason}
|
||||
case openai.ModerationResultReview:
|
||||
return &CommentModerationReviewError{Reason: resp.Reason}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return s.moderateTextWithTencent(ctx, content,
|
||||
func(reason string) error { return &CommentModerationRejectedError{Reason: reason} },
|
||||
func(reason string) error { return &CommentModerationReviewError{Reason: reason} },
|
||||
)
|
||||
}
|
||||
|
||||
func (s *PostAIService) ModerateImage(ctx context.Context, imageURL string) error {
|
||||
if !s.IsEnabled() {
|
||||
if !s.isOpenAIEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := s.openAIClient.ModerateImage(ctx, imageURL)
|
||||
if err != nil {
|
||||
if s.openAIClient.Config().StrictModeration {
|
||||
if s.strictMode {
|
||||
return err
|
||||
}
|
||||
zap.L().Warn("AI image moderation failed, fallback allow",
|
||||
@@ -266,22 +298,69 @@ func (s *PostAIService) ModerateBio(ctx context.Context, bio string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := s.openAIClient.ModerateBio(ctx, bio)
|
||||
if s.isOpenAIEnabled() {
|
||||
resp, err := s.openAIClient.ModerateBio(ctx, bio)
|
||||
if err != nil {
|
||||
zap.L().Warn("AI bio moderation failed, trying Tencent fallback",
|
||||
zap.Error(err),
|
||||
)
|
||||
return s.moderateTextWithTencent(ctx, bio,
|
||||
func(reason string) error { return &BioModerationRejectedError{Reason: reason} },
|
||||
func(reason string) error { return &BioModerationReviewError{Reason: reason} },
|
||||
)
|
||||
}
|
||||
|
||||
switch resp.Result {
|
||||
case openai.ModerationResultBlock:
|
||||
return &BioModerationRejectedError{Reason: resp.Reason}
|
||||
case openai.ModerationResultReview:
|
||||
return &BioModerationReviewError{Reason: resp.Reason}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return s.moderateTextWithTencent(ctx, bio,
|
||||
func(reason string) error { return &BioModerationRejectedError{Reason: reason} },
|
||||
func(reason string) error { return &BioModerationReviewError{Reason: reason} },
|
||||
)
|
||||
}
|
||||
|
||||
func (s *PostAIService) moderateTextWithTencent(
|
||||
ctx context.Context,
|
||||
text string,
|
||||
rejectFactory func(string) error,
|
||||
reviewFactory func(string) error,
|
||||
) error {
|
||||
if !s.isTencentEnabled() {
|
||||
if s.strictMode {
|
||||
return fmt.Errorf("no moderation service available")
|
||||
}
|
||||
zap.L().Warn("No moderation service available, fallback allow")
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := s.tencentClient.TextModeration(ctx, text)
|
||||
if err != nil {
|
||||
if s.openAIClient.Config().StrictModeration {
|
||||
if s.strictMode {
|
||||
return err
|
||||
}
|
||||
zap.L().Warn("AI bio moderation failed, fallback allow",
|
||||
zap.L().Warn("Tencent TMS moderation failed, fallback allow",
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
switch resp.Result {
|
||||
case openai.ModerationResultBlock:
|
||||
return &BioModerationRejectedError{Reason: resp.Reason}
|
||||
case openai.ModerationResultReview:
|
||||
return &BioModerationReviewError{Reason: resp.Reason}
|
||||
reason := resp.Label
|
||||
if len(resp.Keywords) > 0 {
|
||||
reason = resp.Label + ": " + strings.Join(resp.Keywords, ", ")
|
||||
}
|
||||
|
||||
switch resp.Suggestion {
|
||||
case tencent.SuggestionBlock:
|
||||
return rejectFactory(reason)
|
||||
case tencent.SuggestionReview:
|
||||
return reviewFactory(reason)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user