Update
This commit is contained in:
@@ -37,7 +37,7 @@ export class CommentWriterService {
|
||||
tone: dto.tone,
|
||||
});
|
||||
|
||||
// console.log({system, user})
|
||||
this.logger.debug({dto, system, user})
|
||||
|
||||
const res = await provider.complete(
|
||||
[
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
// services/content-safety.service.ts
|
||||
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
|
||||
import {Language} from "../../../common/interfaces/language.prompt.interface";
|
||||
|
||||
export interface SafetyCheckResult {
|
||||
safe: boolean;
|
||||
blockReason?: string;
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ContentSafetyService {
|
||||
private readonly logger = new Logger(ContentSafetyService.name);
|
||||
|
||||
// HARD BLOCKS — không được phép dù bật bất cứ tone nào
|
||||
private readonly HARD_BLOCK_PATTERNS = [
|
||||
// Slurs (đã masked, bạn nên fill list thực tế trong production)
|
||||
/\bn[i1]gg[ae]r/i,
|
||||
/\bf[a4]gg[o0]t/i,
|
||||
/\btr[a4]nn[yi]/i,
|
||||
/\bret[a4]rd/i,
|
||||
// Threats
|
||||
/\b(kill|murder|shoot|behead|stab)\s+(you|him|her|them)/i,
|
||||
// CSAM-related (zero tolerance)
|
||||
/\b(child|minor|kid|underage)\s+(porn|sex|nude)/i,
|
||||
// Doxxing patterns
|
||||
/\b\d{3}-\d{2}-\d{4}\b/, // SSN format
|
||||
];
|
||||
|
||||
// WARNING patterns — log nhưng không block
|
||||
private readonly WARNING_PATTERNS = [
|
||||
/\b@\w+\b/g, // mentions cụ thể (có thể là attack vào người)
|
||||
];
|
||||
|
||||
/**
|
||||
* Check input topic — có nên gen content cho topic này không?
|
||||
*/
|
||||
checkInput(topic: string, allowEdgy: boolean): SafetyCheckResult {
|
||||
const warnings: string[] = [];
|
||||
|
||||
// Hard block patterns
|
||||
for (const pattern of this.HARD_BLOCK_PATTERNS) {
|
||||
if (pattern.test(topic)) {
|
||||
return {
|
||||
safe: false,
|
||||
blockReason: 'Input contains prohibited content (hate speech, threats, or illegal content)',
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check mentions
|
||||
const mentions = topic.match(/@\w+/g);
|
||||
if (mentions && mentions.length > 0 && allowEdgy) {
|
||||
warnings.push(
|
||||
`Topic mentions specific users (${mentions.join(', ')}). Edgy tones target IDEAS not PEOPLE. Use carefully.`,
|
||||
);
|
||||
}
|
||||
|
||||
return { safe: true, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check output — content AI đã gen có safe không?
|
||||
*/
|
||||
checkOutput(content: string, language: Language): SafetyCheckResult {
|
||||
const warnings: string[] = [];
|
||||
|
||||
// Hard block check
|
||||
for (const pattern of this.HARD_BLOCK_PATTERNS) {
|
||||
if (pattern.test(content)) {
|
||||
return {
|
||||
safe: false,
|
||||
blockReason: 'Output contains prohibited language',
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Excessive aggression heuristic
|
||||
const fuckCount = (content.match(/fuck/gi) || []).length;
|
||||
if (fuckCount > 5) {
|
||||
warnings.push(`Very high profanity density (${fuckCount} f-words). Consider lower intensity.`);
|
||||
}
|
||||
|
||||
return { safe: true, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw nếu unsafe (dùng cho controller).
|
||||
*/
|
||||
assertSafe(topic: string, allowEdgy: boolean): void {
|
||||
const result = this.checkInput(topic, allowEdgy);
|
||||
if (!result.safe) {
|
||||
throw new BadRequestException(result.blockReason);
|
||||
}
|
||||
if (result.warnings.length > 0) {
|
||||
this.logger.warn(`Safety warnings: ${result.warnings.join('; ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
// services/provider-router.service.ts
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ContentStyle } from '../enum/style.enum';
|
||||
import { ProviderName } from '../providers/ai-provider.factory';
|
||||
import {Injectable} from '@nestjs/common';
|
||||
import {ContentStyle} from '../enum/style.enum';
|
||||
import {ProviderName} from '../providers/ai-provider.factory';
|
||||
import {Language} from "../../../common/interfaces/language.prompt.interface";
|
||||
import {ContentTone, isEdgyTone} from "../enum/tone.enum";
|
||||
|
||||
interface ProviderPair {
|
||||
writer: ProviderName;
|
||||
@@ -49,10 +50,37 @@ export class ProviderRouterService {
|
||||
language: Language;
|
||||
contentType: ContentType;
|
||||
style?: ContentStyle;
|
||||
tone?: string;
|
||||
tone?: ContentTone;
|
||||
}): RoutingDecision {
|
||||
const { language, contentType, style, tone } = params;
|
||||
|
||||
// 🔥 EDGY TONES: route mạnh sang Grok (EN) hoặc DeepSeek (others)
|
||||
// GPT thường refuse hoặc water down → tránh
|
||||
if (tone && isEdgyTone(tone)) {
|
||||
if (language === 'en') {
|
||||
return {
|
||||
writer: 'grok',
|
||||
reviewer: 'deepseek',
|
||||
useXEnrichment: false,
|
||||
reason: `Edgy tone (${tone}) EN: Grok (handles edge best)`,
|
||||
};
|
||||
}
|
||||
if (tone === ContentTone.SPICY) {
|
||||
return {
|
||||
writer: 'openai',
|
||||
reviewer: 'deepseek',
|
||||
useXEnrichment: false,
|
||||
reason: `Edgy tone (${tone}) EN: Grok (handles edge best)`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
writer: 'deepseek',
|
||||
reviewer: 'deepseek',
|
||||
useXEnrichment: false,
|
||||
reason: `Edgy tone (${tone}) ${language}: DeepSeek (less refusal)`,
|
||||
};
|
||||
}
|
||||
|
||||
// === ENGLISH ===
|
||||
if (language === 'en') {
|
||||
// Breaking news EN -> Grok (real-time + X-native)
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
// services/quote-writer.service.ts
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AIProviderFactory } from '../providers/ai-provider.factory';
|
||||
import { ProviderRouterService } from './provider-router.service';
|
||||
import { LengthStrategyService } from './length-strategy.service';
|
||||
import { ReviewerService } from './reviewer.service';
|
||||
import { GenerateQuoteDto } from '../dto/generate-quote.dto';
|
||||
import { buildQuotePrompt, suggestQuoteType } from '../prompts/quote.templates';
|
||||
import { AccountTier } from '../enum/account-tier.enum';
|
||||
import { Platform } from '../enum/platform.enum';
|
||||
import { ContentStyle } from '../enum/style.enum';
|
||||
import { ContentTone } from '../enum/tone.enum';
|
||||
import {Injectable, Logger} from '@nestjs/common';
|
||||
import {ConfigService} from '@nestjs/config';
|
||||
import {AIProviderFactory} from '../providers/ai-provider.factory';
|
||||
import {ProviderRouterService} from './provider-router.service';
|
||||
import {LengthStrategyService} from './length-strategy.service';
|
||||
import {ReviewerService} from './reviewer.service';
|
||||
import {GenerateQuoteDto} from '../dto/generate-quote.dto';
|
||||
import {buildQuotePrompt, suggestQuoteType} from '../prompts/quote.templates';
|
||||
import {AccountTier} from '../enum/account-tier.enum';
|
||||
import {Platform} from '../enum/platform.enum';
|
||||
import {ContentStyle} from '../enum/style.enum';
|
||||
import {ContentTone, isEdgyTone} from '../enum/tone.enum';
|
||||
import {calculateTokenBudget} from "../../../common/utils/token-calculator";
|
||||
import {QuoteType} from "../enum/quote-type.enum";
|
||||
import {ContentSafetyService} from "./content-safety.service";
|
||||
|
||||
@Injectable()
|
||||
export class QuoteWriterService {
|
||||
@@ -24,6 +25,7 @@ export class QuoteWriterService {
|
||||
private lengthStrategy: LengthStrategyService,
|
||||
private reviewer: ReviewerService,
|
||||
private config: ConfigService,
|
||||
private safety: ContentSafetyService
|
||||
) {}
|
||||
|
||||
async generateQuote(dto: GenerateQuoteDto) {
|
||||
@@ -32,6 +34,10 @@ export class QuoteWriterService {
|
||||
const quoteType = dto.quoteType ?? suggestQuoteType(dto.originalPost, dto.yourAngle);
|
||||
this.logger.log(`Quote type: ${quoteType}`);
|
||||
|
||||
// 🛡️ Safety check first
|
||||
const isEdgy = dto.tone && isEdgyTone(dto.tone);
|
||||
this.safety.assertSafe(dto.originalPost, !!isEdgy);
|
||||
|
||||
// 2. Tier & length
|
||||
const tier = dto.accountTier ?? this.config.get<AccountTier>('X_ACCOUNT_TIER', AccountTier.PREMIUM);
|
||||
|
||||
@@ -51,7 +57,7 @@ export class QuoteWriterService {
|
||||
contentType: 'comment', // quote giống comment hơn post về routing
|
||||
tone: dto.tone,
|
||||
});
|
||||
console.log({lengthDecision,tier, budget,providerDecision});
|
||||
this.logger.debug({lengthDecision,tier, budget,providerDecision});
|
||||
// 4. Build prompt
|
||||
const { system, user } = buildQuotePrompt({
|
||||
originalPost: dto.originalPost,
|
||||
@@ -64,7 +70,7 @@ export class QuoteWriterService {
|
||||
lengthRange: lengthDecision.range,
|
||||
});
|
||||
|
||||
console.log({providerDecision, system, user})
|
||||
this.logger.debug({system, user})
|
||||
|
||||
// 5. Generate
|
||||
const provider = this.factory.get(providerDecision.writer);
|
||||
@@ -118,6 +124,17 @@ export class QuoteWriterService {
|
||||
// quote = quote.substring(0, lengthDecision.hardLimit);
|
||||
// }
|
||||
|
||||
// 🛡️ Check output
|
||||
const outputSafety = this.safety.checkOutput(quote, dto.language);
|
||||
if (!outputSafety.safe) {
|
||||
this.logger.error(`Unsafe output detected: ${outputSafety.blockReason}`);
|
||||
// Retry với tone lower-intensity
|
||||
return this.generateQuote({
|
||||
...dto,
|
||||
tone: this.downgradeTone(dto.tone!),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
quote,
|
||||
quoteType,
|
||||
@@ -129,6 +146,16 @@ export class QuoteWriterService {
|
||||
};
|
||||
}
|
||||
|
||||
private downgradeTone(tone: ContentTone): ContentTone {
|
||||
const downgrade: Partial<Record<ContentTone, ContentTone>> = {
|
||||
[ContentTone.SAVAGE]: ContentTone.AGGRESSIVE,
|
||||
[ContentTone.INFLAMMATORY]: ContentTone.SPICY,
|
||||
[ContentTone.PROFANE]: ContentTone.AGGRESSIVE,
|
||||
[ContentTone.AGGRESSIVE]: ContentTone.SPICY,
|
||||
[ContentTone.SPICY]: ContentTone.CASUAL,
|
||||
};
|
||||
return downgrade[tone] ?? ContentTone.CASUAL;
|
||||
}
|
||||
/**
|
||||
* Generate nhiều variants để bạn chọn bài hay nhất.
|
||||
*/
|
||||
|
||||
@@ -26,7 +26,7 @@ export class StyleDetectorService {
|
||||
[ContentStyle.GENERAL]: /.^/, // never match
|
||||
};
|
||||
|
||||
private readonly toneKeywords: Record<ContentTone, RegExp> = {
|
||||
private readonly toneKeywords: Partial<Record<ContentTone, RegExp>> = {
|
||||
[ContentTone.URGENT]: /\b(urgent|now|🔴|immediately|just dropped|breaking|họp báo|cảnh báo|khẩn|quan trọng| thông báo gấp|alert)\b/i,
|
||||
[ContentTone.HYPE]: /\b(lfg|huge|massive|insane|alpha|don't miss|🚀|🔥)\b/i,
|
||||
[ContentTone.HUMOROUS]: /\b(lol|funny|joke|meme|plot twist|not me|understood the assignment|lmao|bruh|no way|bestie)\b/i,
|
||||
|
||||
Reference in New Issue
Block a user