Update
This commit is contained in:
@@ -9,8 +9,10 @@ import {Context, Telegraf} from "telegraf";
|
||||
import {Logger} from "@nestjs/common";
|
||||
import {QuoteWriterService} from "./services/quote-writer.service";
|
||||
import {GenerateQuoteDto} from "./dto/generate-quote.dto";
|
||||
import {TextUtil} from "../../common/utils/text.util";
|
||||
import {isEmpty} from "lodash";
|
||||
import {SqsPostService} from "../sqs-module/sqs.post.service";
|
||||
import {_getTweetIdFromUrl, rand} from "../../shared/helper";
|
||||
import {XCacheService} from "../x-cache/x-cache.service";
|
||||
|
||||
|
||||
@Processor('comment_writer_queue')
|
||||
@@ -22,8 +24,8 @@ export class CommentWriterProcessor extends WorkerHost {
|
||||
private readonly commentWriterService: CommentWriterService,
|
||||
private readonly xreader: XReaderService,
|
||||
@InjectBot() private readonly bot: Telegraf<Context>,
|
||||
//@InjectQueue('comment_writer_completed_queue') private readonly aiCommentWriteCompletedQueue: Queue,
|
||||
|
||||
private readonly sqsPostService: SqsPostService,
|
||||
private readonly xCache: XCacheService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -47,11 +49,71 @@ export class CommentWriterProcessor extends WorkerHost {
|
||||
const topic = summary || title;
|
||||
let pgPostCreateDto!: PostCreateInput;
|
||||
|
||||
console.log(`CommentWriterProcessor_processing_${job.name}`);
|
||||
this.logger.log(`CommentWriterProcessor_processing_${job.name}`);
|
||||
this.logger.debug(job.data)
|
||||
switch (job.name) {
|
||||
case 'generate_comment_twitter': {
|
||||
case 'GENERATE_AUTO_COMMENT_TWITTER': {
|
||||
let {tweetUrl, tweetText, angle, language, tone, x_username} = job.data;
|
||||
if (isEmpty(tweetUrl) && isEmpty(tweetText)) {
|
||||
this.logger.error(`Job thiếu payload`);
|
||||
await this.bot.telegram.sendMessage(telegramChatId, `Job thiếu payload`)
|
||||
return {};
|
||||
}
|
||||
|
||||
const xpost = await this.xreader.readXPost(url);
|
||||
//Kiểm tra đã comment chưa ?
|
||||
const tweetId = _getTweetIdFromUrl(tweetUrl);
|
||||
const hadCmt = await this.xCache.hasComment(tweetId, x_username);
|
||||
if (hadCmt) {
|
||||
this.logger.error(`Đã comment rồi`);
|
||||
await this.bot.telegram.sendMessage(telegramChatId, `Đã comment rồi`)
|
||||
return {}
|
||||
}
|
||||
|
||||
|
||||
if (isEmpty(tweetText)) {
|
||||
const xpost = await this.xreader.readXPost(tweetUrl);
|
||||
tweetText = xpost.text || '';
|
||||
if (!language) {
|
||||
language = xpost.lang || 'en';
|
||||
}
|
||||
}
|
||||
if (isEmpty(tweetText)) {
|
||||
this.logger.error(`Tweet text = Empty`);
|
||||
await this.bot.telegram.sendMessage(telegramChatId, `Tweet text = Empty`)
|
||||
return {};
|
||||
}
|
||||
const dto = {
|
||||
originalPost: tweetText,
|
||||
angle,
|
||||
language: language,
|
||||
tone,
|
||||
};
|
||||
this.logger.debug(`==> Bắt đầu viết auto comment: ${tweetUrl}`, dto);
|
||||
|
||||
const res = await this.commentWriterService.generateComment(dto, false);
|
||||
this.logger.debug(`==> Đã viết auto comment xong:`, dto, res);
|
||||
|
||||
await this.sqsPostService.sendMessage(x_username, {
|
||||
// ...res,
|
||||
telegramChatId,
|
||||
content: res.comment,
|
||||
tweetUrl,
|
||||
job_type: 'X_POSTER_REPLY',
|
||||
type: 'X_POSTER_REPLY'
|
||||
},
|
||||
rand(42, 84)
|
||||
);
|
||||
await this.xCache.setAlreadyComment(tweetId, x_username);
|
||||
|
||||
this.logger.log(`Đã gửi comment auto sang queue thành công.`);
|
||||
await this.bot.telegram.sendMessage(telegramChatId, `
|
||||
Đã gửi comment auto sang queue thành công. M:${res.model}- T:${dto?.tone} A:${dto?.angle} `)
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'generate_comment_twitter': {
|
||||
const xpost: any = await this.xreader.readXPost(url);
|
||||
|
||||
const dto: GenerateCommentDto = {
|
||||
originalPost: xpost.text,
|
||||
@@ -81,7 +143,7 @@ export class CommentWriterProcessor extends WorkerHost {
|
||||
// }, {attempts: 1, backoff: 5000, removeOnComplete: true,});
|
||||
|
||||
await this.bot.telegram.sendMessage(telegramChatId, `
|
||||
Đã viết reply xong ...\nmodel: ${aiWriterResult.model} - tokenUsed: ${aiWriterResult.tokensUsed}`);
|
||||
Đã viết reply xong ...\nmodel: ${aiWriterResult.model} - tokenUsed: ${aiWriterResult.tokensUsed}\ntransVi: ${aiWriterResult.commentTransVi}`);
|
||||
// const _url = url.indexOf('?s=20') > -1 ? url : `${url}?s=20`;
|
||||
// console.log({_url});
|
||||
|
||||
@@ -144,7 +206,7 @@ export class CommentWriterProcessor extends WorkerHost {
|
||||
|
||||
const adminChatId = process.env.TELEGRAM_ADMIN_ID || 0;
|
||||
await this.bot.telegram.sendMessage(telegramChatId || adminChatId, `
|
||||
Đã viết reply xong ...\ntweetId=${tweetId}\nmodel: ${aiWriterResult.model} }
|
||||
Đã viết reply xong ...\ntweetId=${tweetId}\nmodel: ${aiWriterResult.model}\ndich:${aiWriterResult.commentTransVi}
|
||||
`);
|
||||
|
||||
await this.bot.telegram.sendMessage(telegramChatId || adminChatId,
|
||||
@@ -186,7 +248,7 @@ export class CommentWriterProcessor extends WorkerHost {
|
||||
}
|
||||
case 'generate_quote_twitter': {
|
||||
this.logger.debug('===>generate_quote_twitter:', url);
|
||||
const xpost = await this.xreader.readXPost(url, xreaderMode, telegramChatId);
|
||||
const xpost: any = await this.xreader.readXPost(url, xreaderMode, telegramChatId);
|
||||
|
||||
const originalAuthor = `${xpost.author} ${xpost.handle}`;
|
||||
const dto: GenerateQuoteDto = {
|
||||
@@ -284,9 +346,7 @@ export class CommentWriterProcessor extends WorkerHost {
|
||||
|
||||
this.logger.log({aiWriterResult});
|
||||
const sendId = telegramChatId || process.env.TELEGRAM_ADMIN_ID || 0;
|
||||
await this.bot.telegram.sendMessage(sendId, `
|
||||
Đã viết quote xong ...\nmodel: ${aiWriterResult.model} - type: ${aiWriterResult.quoteType}
|
||||
`);
|
||||
|
||||
|
||||
//
|
||||
let finalQuote = aiWriterResult.quote;
|
||||
@@ -294,13 +354,18 @@ export class CommentWriterProcessor extends WorkerHost {
|
||||
finalQuote += `\n\nQuote:"${dto.originalPost}\n\n${dto.originalAuthor}`
|
||||
}
|
||||
//xoá url trong bài vì tốn 0,2$ cho bài có url
|
||||
const finalQuoteCleanUrl = TextUtil.removeAllUrl(finalQuote)
|
||||
// const finalQuoteCleanUrl = TextUtil.removeAllUrl(finalQuote)
|
||||
|
||||
const X_USERS = process.env.TWITTER_USERNAMES!.split(',');
|
||||
|
||||
await this.bot.telegram.sendMessage(sendId, `
|
||||
Đã viết quote xong ...
|
||||
model: ${aiWriterResult.model}
|
||||
type: ${aiWriterResult.quoteType}
|
||||
dịch: ${aiWriterResult.quoteTransVi}
|
||||
`);
|
||||
await this.bot.telegram.sendMessage(
|
||||
sendId,
|
||||
finalQuoteCleanUrl,
|
||||
finalQuote,
|
||||
{
|
||||
// parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
|
||||
@@ -5,12 +5,16 @@ import {CommentWriterService} from './services/comment-writer.service';
|
||||
import {GenerateContentDto} from './dto/generate-content.dto';
|
||||
import {GenerateCommentDto} from './dto/generate-comment.dto';
|
||||
import {IAIGrokProvider} from "./interfaces/ai-provider.interface";
|
||||
import {AiTranslatorDto} from "./dto/ai-translator.dto";
|
||||
import {TranslatorService} from "./services/translator.service";
|
||||
import {GenerateCommentAdapterExtDto} from "./dto/generate-comment-adapter-ext.dto";
|
||||
|
||||
@Controller('content-writer')
|
||||
export class ContentWriterController {
|
||||
constructor(
|
||||
private readonly writerService: ContentWriterService,
|
||||
private readonly commentService: CommentWriterService,
|
||||
private readonly translatorService: TranslatorService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -29,12 +33,22 @@ export class ContentWriterController {
|
||||
}
|
||||
|
||||
@Post('comment')
|
||||
generateComment(@Body() dto: GenerateCommentDto) {
|
||||
return this.commentService.generateComment(dto);
|
||||
generateComment(@Body() dto: GenerateCommentAdapterExtDto) {
|
||||
const dto0: GenerateCommentDto = {
|
||||
...dto,
|
||||
language: dto.lang,
|
||||
originalPost: dto.tweet_text
|
||||
}
|
||||
return this.commentService.generateComment(dto0);
|
||||
}
|
||||
|
||||
@Post('comment/variants')
|
||||
generateCommentVariants(@Body() dto: GenerateCommentDto) {
|
||||
// return this.commentService.generateVariants(dto, 3);
|
||||
}
|
||||
|
||||
@Post('ai-translate')
|
||||
aiTranslator(@Body() dto: AiTranslatorDto) {
|
||||
return this.translatorService.translator(dto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import {Global, Module} from '@nestjs/common';
|
||||
import {Module} from '@nestjs/common';
|
||||
import {AIService} from "../../shared/ai.service";
|
||||
import {ContentWriterProcessor} from "./content.writer.processor";
|
||||
import {PgPostService} from "../../shared/pg.post.service";
|
||||
import {BullModule} from "@nestjs/bullmq";
|
||||
import {PublishPageService} from "../social-api/publish.page.service";
|
||||
import {SocialModule} from "../social-api/social.module";
|
||||
import {ContentWriterController} from "./content-writer.controller";
|
||||
import {AIProviderFactory} from "./providers/ai-provider.factory";
|
||||
@@ -22,6 +21,8 @@ import {LengthStrategyService} from "./services/length-strategy.service";
|
||||
import {QuoteWriterService} from "./services/quote-writer.service";
|
||||
import {SqsPostService} from "../sqs-module/sqs.post.service";
|
||||
import {ContentSafetyService} from "./services/content-safety.service";
|
||||
import {TranslatorService} from "./services/translator.service";
|
||||
import {GoogleProvider} from "./providers/google.provider";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -43,6 +44,7 @@ import {ContentSafetyService} from "./services/content-safety.service";
|
||||
OpenAIProvider,
|
||||
DeepSeekProvider,
|
||||
GrokProvider,
|
||||
GoogleProvider,
|
||||
AIProviderFactory,
|
||||
ProviderRouterService,
|
||||
CommentWriterProcessor,
|
||||
@@ -51,8 +53,9 @@ import {ContentSafetyService} from "./services/content-safety.service";
|
||||
QuoteWriterService,
|
||||
SqsPostService,
|
||||
ContentSafetyService,
|
||||
TranslatorService,
|
||||
],
|
||||
exports: [GrokProvider, ContentWriterService],
|
||||
exports: [GrokProvider, ContentWriterService, TranslatorService],
|
||||
|
||||
})
|
||||
export class ContentWriterModule {
|
||||
|
||||
@@ -14,6 +14,8 @@ import {AccountTier} from "./enum/account-tier.enum";
|
||||
import {ConfigService} from "@nestjs/config";
|
||||
import {LengthStrategyService} from "./services/length-strategy.service";
|
||||
import {calculateTokenBudget} from "../../common/utils/token-calculator";
|
||||
import {AiTranslatorDto} from "./dto/ai-translator.dto";
|
||||
import {buildReviewerPrompt, LANGUAGE_NAMES} from "./prompts/templates";
|
||||
|
||||
@Injectable()
|
||||
export class ContentWriterService {
|
||||
@@ -33,6 +35,10 @@ export class ContentWriterService {
|
||||
async getGrokAI() {
|
||||
return this.factory.get('grok');
|
||||
}
|
||||
async getGptAI() {
|
||||
return this.factory.get('openai');
|
||||
}
|
||||
|
||||
|
||||
async generate(
|
||||
dto: GenerateContentDto,
|
||||
|
||||
@@ -14,14 +14,20 @@ import {ContentStyle} from "./enum/style.enum";
|
||||
import {ContentTone} from "./enum/tone.enum";
|
||||
import {XStrategy} from "../social-api/x-router.service";
|
||||
import {SqsPostService} from "../sqs-module/sqs.post.service";
|
||||
import {TranslatorService} from "./services/translator.service";
|
||||
import {CommentWriterService} from "./services/comment-writer.service";
|
||||
import {XReaderService} from "../x-reader/x-reader.service";
|
||||
import {Logger} from "@nestjs/common";
|
||||
|
||||
@Processor('content_writer_queue')
|
||||
export class ContentWriterProcessor extends WorkerHost {
|
||||
private readonly logger = new Logger("ContentWriterProcessor");
|
||||
constructor(
|
||||
private aiService: AIService,
|
||||
private readonly writerService: ContentWriterService,
|
||||
private readonly styleDetectorService: StyleDetectorService,
|
||||
private readonly pgPostService: PgPostService,
|
||||
private readonly translatorService: TranslatorService,
|
||||
@InjectBot() private readonly bot: Telegraf<Context>,
|
||||
// private readonly managerService: ManagerService,
|
||||
@InjectQueue('content_writer_completed_queue') private readonly fbContentCompletedQueue: Queue,
|
||||
@@ -44,7 +50,8 @@ export class ContentWriterProcessor extends WorkerHost {
|
||||
enableReview,
|
||||
language,
|
||||
tone,
|
||||
postLength
|
||||
postLength,
|
||||
style,
|
||||
}
|
||||
const aiWriterResult = await this.writerService.generate(dto, false, 'openai', 'deepseek');
|
||||
// console.log({aiWriterResult});
|
||||
@@ -79,7 +86,6 @@ export class ContentWriterProcessor extends WorkerHost {
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'generate_post_telegram': {
|
||||
isAutoPublish = true;
|
||||
const topicLen = topic.length;
|
||||
@@ -161,11 +167,24 @@ export class ContentWriterProcessor extends WorkerHost {
|
||||
// let finalContent = aiWriterResult.content;
|
||||
const post = await this.pgPostService.createPost(pgPostCreateDto);
|
||||
if (!isAutoPublish) {
|
||||
let contentTransVi = '';
|
||||
let contentTransModel = '';
|
||||
if (language !== 'vi') {
|
||||
const resTranslate = await this.translatorService.translator({
|
||||
text: pgPostCreateDto.content,
|
||||
target_lang: "vi",
|
||||
target_model: 'google'
|
||||
})
|
||||
contentTransVi = resTranslate.content;
|
||||
contentTransModel = resTranslate.model;
|
||||
}
|
||||
await this.fbContentCompletedQueue.add('generate_post_completed', {
|
||||
id: post.id,
|
||||
name: 'generate_post_completed',
|
||||
needConfirm: 1,
|
||||
content: pgPostCreateDto.content,
|
||||
contentTransVi,
|
||||
contentTransModel,
|
||||
autoPublish: false,
|
||||
telegramChatId,
|
||||
xSubmitProvider: post.id % 2 === 0 ? XStrategy.BROWSER_ONLY : XStrategy.API_ONLY, //cứ 3post api, có 1 post browser
|
||||
@@ -180,7 +199,7 @@ export class ContentWriterProcessor extends WorkerHost {
|
||||
content: pgPostCreateDto.content,
|
||||
autoPublish,
|
||||
telegramChatId,
|
||||
publishTo: ['x', 'fb'],
|
||||
publishTo: [ 'fb'],
|
||||
xSubmitProvider: post.id % 3 === 0 ? XStrategy.API_FIRST : XStrategy.BROWSER_ONLY, //cứ 3post api, có 1 post browser
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// dto/generate-content.dto.ts
|
||||
import {IsOptional, IsString, MaxLength} from 'class-validator';
|
||||
import * as languagePromptInterface from "../../../common/interfaces/language.prompt.interface";
|
||||
import * as aiProviderFactory from "../providers/ai-provider.factory";
|
||||
|
||||
export class AiTranslatorDto {
|
||||
@IsString()
|
||||
@MaxLength(5000)
|
||||
text: string; // chủ đề / input thô từ user
|
||||
|
||||
@IsString()
|
||||
target_lang: languagePromptInterface.Language; // 'vi' | 'en' ... default 'en'
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
target_model?: aiProviderFactory.ProviderName;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// dto/generate-comment.dto.ts
|
||||
import { IsEnum, IsString, IsOptional, MaxLength } from 'class-validator';
|
||||
import { ContentTone } from '../enum/tone.enum';
|
||||
import * as languagePromptInterface from "../../../common/interfaces/language.prompt.interface";
|
||||
import {AngleEnum} from "../enum/angle.enum";
|
||||
import {Transform} from "class-transformer";
|
||||
|
||||
export class GenerateCommentAdapterExtDto {
|
||||
@IsString()
|
||||
@MaxLength(5000)
|
||||
tweet_text: string; // nội dung bài X gốc
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Transform(({ value }) => value?.trim().toLowerCase())
|
||||
angle?: AngleEnum; // góc nhìn muốn comment: "agree", "challenge", "add-info", "funny"
|
||||
|
||||
@IsString()
|
||||
lang: languagePromptInterface.Language;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => value?.trim().toLowerCase())
|
||||
tone?: ContentTone;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
persona?: string; // "crypto trader", "news analyst"...
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import {AngleEnum} from "../enum/angle.enum";
|
||||
|
||||
export class GenerateCommentDto {
|
||||
@IsString()
|
||||
@MaxLength(3000)
|
||||
@MaxLength(5000)
|
||||
originalPost: string; // nội dung bài X gốc
|
||||
|
||||
@IsOptional()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {ContentTone} from "./tone.enum";
|
||||
|
||||
export enum AngleEnum {
|
||||
NATURAL = 'natural',
|
||||
AGREE = 'agree',
|
||||
CHALLENGE = 'challenge',
|
||||
ADD_INFO = 'add_info',
|
||||
@@ -33,6 +34,7 @@ export function isEMPATHYToneAngle(angle: AngleEnum): boolean {
|
||||
}
|
||||
|
||||
export const ANGLE_HINTS: Record<AngleEnum, string> = {
|
||||
[AngleEnum.NATURAL]: 'natural reaction.',
|
||||
[AngleEnum.AGREE]: 'I agree and would like to add a small point to support my argument.',
|
||||
[AngleEnum.CHALLENGE]: 'Disagree or add further nuance',
|
||||
[AngleEnum.ADD_INFO]: 'additional useful related information',
|
||||
@@ -61,6 +63,7 @@ export const get_ANGLE_HINTS_TELEGRAM_BUTTON=(tone:ContentTone)=>{
|
||||
return buttons;
|
||||
}
|
||||
export const DEFAULT_ANGLE_HINTS_TELEGRAM_BUTTON: Partial<Record<AngleEnum, Object>> = {
|
||||
[AngleEnum.NATURAL]: {text: 'phản ứng tự nhiên'},
|
||||
[AngleEnum.AGREE]: {text: 'Đồng ý'},
|
||||
[AngleEnum.CHALLENGE]: {text: 'Không đồng ý'},
|
||||
[AngleEnum.ADD_INFO]: {text: 'thêm thông tin liên quan hữu ích'},
|
||||
|
||||
@@ -70,7 +70,8 @@ export function buildCommentPrompt(params: {
|
||||
: '';
|
||||
const budget = calculateLengthBudget(Platform.X, params.language);
|
||||
|
||||
const angleInstruction = getAngleHint(params.angle!, params.language, '- ANGLE:');
|
||||
const angle = params.angle ? params.angle : AngleEnum.NATURAL;
|
||||
const angleInstruction = getAngleHint(angle, params.language, '- ANGLE:');
|
||||
|
||||
console.debug({toneInstruction, angleInstruction});
|
||||
|
||||
@@ -92,5 +93,8 @@ export function buildCommentPrompt(params: {
|
||||
`- Output ONLY the reply text.`,
|
||||
].filter(Boolean).join('\n');
|
||||
|
||||
params.angle = angle;
|
||||
params.tone = tone;
|
||||
|
||||
return {system: system, user};
|
||||
}
|
||||
|
||||
@@ -294,11 +294,11 @@ export function getEmpathyAngleInstruction(
|
||||
}
|
||||
|
||||
export function getAngleHint(angle: AngleEnum, language: Language, prefix = '- '): string {
|
||||
if (!angle) return '';
|
||||
if (!angle) return prefix + ANGLE_HINTS[AngleEnum.NATURAL];
|
||||
if (isEMPATHYToneAngle(angle)) {
|
||||
return getEmpathyAngleInstruction(angle, language)
|
||||
}
|
||||
const agHint = ANGLE_HINTS[language];
|
||||
const agHint = ANGLE_HINTS[angle];
|
||||
return agHint
|
||||
? `${prefix} ${agHint}`
|
||||
: ``;
|
||||
|
||||
@@ -4,8 +4,9 @@ import {OpenAIProvider} from './openai.provider';
|
||||
import {DeepSeekProvider} from './deepseek.provider';
|
||||
import {IAIProvider} from '../interfaces/ai-provider.interface';
|
||||
import {GrokProvider} from "./grok.provider";
|
||||
import {GoogleProvider} from "./google.provider";
|
||||
|
||||
export type ProviderName = 'openai' | 'deepseek' | 'grok';
|
||||
export type ProviderName = 'openai' | 'deepseek' | 'grok' | 'google';
|
||||
|
||||
@Injectable()
|
||||
export class AIProviderFactory {
|
||||
@@ -13,6 +14,7 @@ export class AIProviderFactory {
|
||||
private readonly openai: OpenAIProvider,
|
||||
private readonly deepseek: DeepSeekProvider,
|
||||
private readonly grok: GrokProvider,
|
||||
private readonly google: GoogleProvider,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -24,6 +26,8 @@ export class AIProviderFactory {
|
||||
return this.deepseek;
|
||||
case 'grok':
|
||||
return this.grok;
|
||||
case 'google':
|
||||
return this.google;
|
||||
default:
|
||||
throw new Error(`Unknown AI provider: ${name}`);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export class DeepSeekProvider implements IAIProvider {
|
||||
baseURL: 'https://api.deepseek.com/v1',
|
||||
});
|
||||
this.defaultModel = this.config.get('DEEPSEEK_MODEL', 'deepseek-chat');
|
||||
// this.defaultModel = this.config.get('DEEPSEEK_MODEL', 'deepseek-v4-pro');
|
||||
}
|
||||
|
||||
async complete(messages: AIMessage[], options: AICompletionOptions = {}): Promise<AICompletionResult> {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// providers/grok.provider.ts
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import OpenAI from 'openai';
|
||||
import { IAIProvider, AIMessage, AICompletionOptions, AICompletionResult } from '../interfaces/ai-provider.interface';
|
||||
import {ChatCompletionTool} from "openai/resources/chat/completions/completions";
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class GoogleProvider implements IAIProvider {
|
||||
readonly name = 'google';
|
||||
private readonly logger = new Logger(GoogleProvider.name);
|
||||
private client: OpenAI;
|
||||
private defaultModel: string;
|
||||
|
||||
constructor(private config: ConfigService) {
|
||||
// this.client = new OpenAI({
|
||||
// apiKey: this.config.get('XAI_API_KEY'),
|
||||
// baseURL: 'https://api.x.ai/v1',
|
||||
// });
|
||||
this.client = new OpenAI({
|
||||
baseURL: 'https://openrouter.ai/api/v1',
|
||||
apiKey: this.config.get('OPEN_ROUTER_API_KEY')
|
||||
});
|
||||
// grok-3-mini: rẻ & nhanh; grok-3: mạnh hơn
|
||||
// this.defaultModel = this.config.get('XAI_MODEL', 'x-ai/grok-3-mini');
|
||||
// this.defaultModel = this.config.get('XAI_MODEL', 'x-ai/grok-4.1-fast');
|
||||
// this.defaultModel = this.config.get('OPENAI_MODEL', 'google/gemini-2.5-flash'); // rẻ
|
||||
this.defaultModel = this.config.get('OPENAI_MODEL', 'google/gemini-2.5-flash-lite'); // rẻ
|
||||
// rẻ
|
||||
|
||||
}
|
||||
|
||||
async getModelName() {
|
||||
return this.defaultModel;
|
||||
}
|
||||
|
||||
async complete(
|
||||
messages: AIMessage[],
|
||||
options: AICompletionOptions = {}
|
||||
|
||||
): Promise<AICompletionResult> {
|
||||
const model = options.model ?? this.defaultModel;
|
||||
const res = await this.client.chat.completions.create({
|
||||
model,
|
||||
messages,
|
||||
temperature: options.temperature ?? 0.8,
|
||||
max_tokens: options.maxTokens ?? 400,
|
||||
});
|
||||
|
||||
return {
|
||||
content: res.choices[0]?.message?.content?.trim() ?? '',
|
||||
tokensUsed: res.usage?.total_tokens ?? 0,
|
||||
model,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -18,17 +18,12 @@ export class GrokProvider implements IAIProvider {
|
||||
private defaultModel: string;
|
||||
|
||||
constructor(private config: ConfigService) {
|
||||
// this.client = new OpenAI({
|
||||
// apiKey: this.config.get('XAI_API_KEY'),
|
||||
// baseURL: 'https://api.x.ai/v1',
|
||||
// });
|
||||
this.client = new OpenAI({
|
||||
baseURL: 'https://openrouter.ai/api/v1',
|
||||
apiKey: this.config.get('OPEN_ROUTER_API_KEY')
|
||||
});
|
||||
// grok-3-mini: rẻ & nhanh; grok-3: mạnh hơn
|
||||
// this.defaultModel = this.config.get('XAI_MODEL', 'x-ai/grok-3-mini');
|
||||
this.defaultModel = this.config.get('XAI_MODEL', 'x-ai/grok-4.1-fast');
|
||||
this.defaultModel = this.config.get('OPENAI_MODEL', 'x-ai/grok-4.3');
|
||||
|
||||
}
|
||||
|
||||
async getModelName() {
|
||||
|
||||
@@ -41,7 +41,7 @@ export class OpenAIProvider implements IAIProvider {
|
||||
}
|
||||
|
||||
async complete(messages: AIMessage[], options: AICompletionOptions = {}): Promise<AICompletionResult> {
|
||||
console.log(`OpenAIProvider_complete`);
|
||||
console.log(`OpenAIProvider_complete_begin...`);
|
||||
|
||||
const model = options.model ?? this.defaultModel;
|
||||
try {
|
||||
|
||||
@@ -4,6 +4,8 @@ import {AIProviderFactory} from '../providers/ai-provider.factory';
|
||||
import {GenerateCommentDto} from '../dto/generate-comment.dto';
|
||||
import {buildCommentPrompt} from '../prompts/comment.templates';
|
||||
import {ProviderRouterService} from "./provider-router.service";
|
||||
import {TranslatorService} from "./translator.service";
|
||||
import {getUuid4} from "../../../shared/helper";
|
||||
|
||||
@Injectable()
|
||||
export class CommentWriterService {
|
||||
@@ -12,10 +14,27 @@ export class CommentWriterService {
|
||||
constructor(
|
||||
private factory: AIProviderFactory,
|
||||
private router: ProviderRouterService,
|
||||
private translatorService: TranslatorService
|
||||
) {
|
||||
}
|
||||
|
||||
async generateComment(dto: GenerateCommentDto) {
|
||||
// async generateComment() {}
|
||||
|
||||
async generateComment(
|
||||
dto: GenerateCommentDto,
|
||||
allowTranslateToVi = true
|
||||
): Promise<{
|
||||
commentTransVi?: string;
|
||||
commentTransModel?: string;
|
||||
comment: string;
|
||||
tokensUsed: number;
|
||||
tokenTranslatorUsed?: number;
|
||||
model: string;
|
||||
language: "en" | "vi" | "ja" | "ko" | "cn",
|
||||
input?: GenerateCommentDto,
|
||||
uid:string,
|
||||
}> {
|
||||
// const uid = getUuid4();
|
||||
const decision = this.router.route({
|
||||
language: dto.language,
|
||||
contentType: 'comment',
|
||||
@@ -29,13 +48,7 @@ export class CommentWriterService {
|
||||
const provider = this.factory.get(decision.writer);
|
||||
this.logger.log(`==> Comment routing: ${decision.reason} ==>`);
|
||||
// console.log({dto})
|
||||
const {system, user} = buildCommentPrompt({
|
||||
originalPost: dto.originalPost,
|
||||
angle: dto.angle,
|
||||
language: dto.language,
|
||||
persona: dto.persona,
|
||||
tone: dto.tone,
|
||||
});
|
||||
const {system, user} = buildCommentPrompt(dto);
|
||||
|
||||
this.logger.debug({dto, system, user})
|
||||
|
||||
@@ -54,12 +67,35 @@ export class CommentWriterService {
|
||||
|
||||
// Clean output: bỏ quotes nếu AI lỡ wrap
|
||||
const cleaned = res.content.replace(/^["""']|["""']$/g, '').trim();
|
||||
this.logger.debug({
|
||||
post: dto.originalPost,
|
||||
output: cleaned
|
||||
});
|
||||
//transla if language != vi
|
||||
let commentTransVi = '';
|
||||
let commentTransModel = '';
|
||||
let tokenTranslatorUsed = 0;
|
||||
if (allowTranslateToVi && dto.language !== 'vi') {
|
||||
const resT = await this.translatorService.translator({
|
||||
text: cleaned,
|
||||
target_lang: 'vi',
|
||||
target_model: 'google'
|
||||
})
|
||||
commentTransVi = resT.content;
|
||||
commentTransModel = resT.model;
|
||||
tokenTranslatorUsed = resT.tokensUsed;
|
||||
}
|
||||
|
||||
return {
|
||||
commentTransVi,
|
||||
commentTransModel,
|
||||
comment: cleaned,
|
||||
tokensUsed: res.tokensUsed,
|
||||
tokenTranslatorUsed,
|
||||
model: res.model,
|
||||
language: dto.language,
|
||||
input: dto,
|
||||
uid: '',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,13 @@ import {ProviderName} from '../providers/ai-provider.factory';
|
||||
import {Language} from "../../../common/interfaces/language.prompt.interface";
|
||||
import {ContentTone, isEdgyTone} from "../enum/tone.enum";
|
||||
import {AngleEnum} from "../enum/angle.enum";
|
||||
import {getRandomElement} from "../../../shared/helper";
|
||||
|
||||
interface ProviderPair {
|
||||
writer: ProviderName;
|
||||
reviewer: ProviderName;
|
||||
}
|
||||
export type ContentType = 'post' | 'comment';
|
||||
export type ContentType = 'post' | 'comment' | 'translation';
|
||||
interface RoutingDecision {
|
||||
writer: ProviderName;
|
||||
reviewer: ProviderName;
|
||||
@@ -55,6 +56,24 @@ export class ProviderRouterService {
|
||||
}): RoutingDecision {
|
||||
const { language, contentType, style, tone } = params;
|
||||
|
||||
if(contentType ==='translation') {
|
||||
if (language === 'cn') {
|
||||
// Default EN
|
||||
return {
|
||||
writer: 'deepseek',
|
||||
reviewer: 'deepseek',
|
||||
useXEnrichment: false,
|
||||
reason: 'CN default: GPT reliable',
|
||||
};
|
||||
}
|
||||
return {
|
||||
writer: getRandomElement([ 'openai',]),
|
||||
reviewer: 'openai',
|
||||
useXEnrichment: false,
|
||||
reason: 'EN default: GPT reliable',
|
||||
};
|
||||
}
|
||||
|
||||
if (tone === ContentTone.EMPATHETIC) {
|
||||
return {
|
||||
writer: 'openai', // warmest voice, less "AI-ish"
|
||||
@@ -69,7 +88,7 @@ export class ProviderRouterService {
|
||||
if (tone && isEdgyTone(tone)) {
|
||||
if (language === 'en') {
|
||||
return {
|
||||
writer: 'grok',
|
||||
writer: getRandomElement(['openai', 'google', 'grok']),
|
||||
reviewer: 'deepseek',
|
||||
useXEnrichment: false,
|
||||
reason: `Edgy tone (${tone}) EN: Grok (handles edge best)`,
|
||||
@@ -77,14 +96,14 @@ export class ProviderRouterService {
|
||||
}
|
||||
if (tone === ContentTone.SPICY) {
|
||||
return {
|
||||
writer: 'openai',
|
||||
writer: getRandomElement(['deepseek', 'google', 'openai',]),
|
||||
reviewer: 'deepseek',
|
||||
useXEnrichment: false,
|
||||
reason: `Edgy tone (${tone}) EN: Grok (handles edge best)`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
writer: 'deepseek',
|
||||
writer: getRandomElement(['deepseek', 'google',]),
|
||||
reviewer: 'deepseek',
|
||||
useXEnrichment: false,
|
||||
reason: `Edgy tone (${tone}) ${language}: DeepSeek (less refusal)`,
|
||||
@@ -96,9 +115,9 @@ export class ProviderRouterService {
|
||||
// Breaking news EN -> Grok (real-time + X-native)
|
||||
if (style === ContentStyle.BREAKING_NEWS) {
|
||||
return {
|
||||
writer: 'grok',
|
||||
writer: 'google',
|
||||
reviewer: 'deepseek',
|
||||
useXEnrichment: true,
|
||||
useXEnrichment: false,
|
||||
reason: 'EN breaking news: Grok has real-time X context',
|
||||
};
|
||||
}
|
||||
@@ -106,7 +125,7 @@ export class ProviderRouterService {
|
||||
// Comment EN casual/witty -> Grok
|
||||
if (contentType === 'comment' && tone !== 'professional') {
|
||||
return {
|
||||
writer: 'grok',
|
||||
writer: 'google',
|
||||
reviewer: 'deepseek',
|
||||
useXEnrichment: false,
|
||||
reason: 'EN casual comment: Grok sounds most human on X',
|
||||
|
||||
@@ -14,6 +14,7 @@ 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";
|
||||
import {TranslatorService} from "./translator.service";
|
||||
|
||||
@Injectable()
|
||||
export class QuoteWriterService {
|
||||
@@ -25,10 +26,11 @@ export class QuoteWriterService {
|
||||
private lengthStrategy: LengthStrategyService,
|
||||
private reviewer: ReviewerService,
|
||||
private config: ConfigService,
|
||||
private safety: ContentSafetyService
|
||||
private safety: ContentSafetyService,
|
||||
private translatorService: TranslatorService
|
||||
) {}
|
||||
|
||||
async generateQuote(dto: GenerateQuoteDto, _retryCount = 0) {
|
||||
async generateQuote(dto: GenerateQuoteDto, _retryCount = 0 , allowTranslateToVi =true) {
|
||||
const MAX_RETRIES = 2;
|
||||
|
||||
if (_retryCount >= MAX_RETRIES) {
|
||||
@@ -145,6 +147,21 @@ export class QuoteWriterService {
|
||||
);
|
||||
}
|
||||
|
||||
//transla if language != vi
|
||||
let quoteTransVi = '';
|
||||
let quoteTransModel = '';
|
||||
let tokenTranslatorUsed = 0;
|
||||
if (allowTranslateToVi && dto.language !== 'vi') {
|
||||
const resT = await this.translatorService.translator({
|
||||
text: quote,
|
||||
target_lang: 'vi',
|
||||
target_model: 'google'
|
||||
})
|
||||
quoteTransVi = resT.content;
|
||||
quoteTransModel = resT.model;
|
||||
tokenTranslatorUsed = resT.tokensUsed;
|
||||
}
|
||||
|
||||
return {
|
||||
quote,
|
||||
quoteType,
|
||||
@@ -153,6 +170,9 @@ export class QuoteWriterService {
|
||||
reviewNotes,
|
||||
tokensUsed: totalTokens,
|
||||
model: modelUsed,
|
||||
quoteTransVi,
|
||||
quoteTransModel,
|
||||
tokenTranslatorUsed
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -59,8 +59,8 @@ export class StyleDetectorService {
|
||||
}
|
||||
|
||||
detectLanguageFromTelegramAutoContent(text: string): Language {
|
||||
if (/nhật[ _]bản/i.test(text)) return "ja";
|
||||
if (/#hàn_quốc/i.test(text)) return "ko";
|
||||
// if (/nhật[ _]bản/i.test(text)) return "ja";
|
||||
// if (/#hàn_quốc/i.test(text)) return "ko";
|
||||
|
||||
// return getLanguageByJSTTime();
|
||||
//
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import {AiTranslatorDto} from "../dto/ai-translator.dto";
|
||||
import {Language} from "../../../common/interfaces/language.prompt.interface";
|
||||
import {LANGUAGE_NAMES} from "../prompts/templates";
|
||||
import {Injectable, Logger} from "@nestjs/common";
|
||||
import {AIProviderFactory, ProviderName} from "../providers/ai-provider.factory";
|
||||
import {ProviderRouterService} from "./provider-router.service";
|
||||
|
||||
@Injectable()
|
||||
export class TranslatorService {
|
||||
private readonly logger = new Logger(TranslatorService.name);
|
||||
|
||||
constructor(
|
||||
private factory: AIProviderFactory,
|
||||
private router: ProviderRouterService,
|
||||
) {
|
||||
}
|
||||
async translator(dto: AiTranslatorDto) {
|
||||
this.logger.log(`Translating ...`);
|
||||
const targetLanguage = LANGUAGE_NAMES[dto.target_lang] || LANGUAGE_NAMES['en'];
|
||||
const systemPrompt =
|
||||
`You are an expert X (Twitter) translator.
|
||||
Rules:
|
||||
- Translate naturally and fluently.
|
||||
- Preserve the original tone, emotion, and internet culture.
|
||||
- Keep the post concise and punchy like an actual X post.
|
||||
- Preserve slang, memes, sarcasm, and crypto terminology.
|
||||
- Do NOT over-formalize the translation.
|
||||
- Keep token names, ticker symbols, usernames, hashtags, and project names unchanged.
|
||||
- Preserve emojis, line breaks, and formatting.
|
||||
- Do not add explanations, notes, or extra commentary.
|
||||
- Avoid robotic or textbook-style translation.
|
||||
- Output ONLY the translated text.`;
|
||||
const USER_PROMPTS_HINT: Record<Language, string> = {
|
||||
'en':'Translate to English:',
|
||||
'cn':'Translate to Chinese:',
|
||||
'vi':'Translate to Vietnamese:',
|
||||
'ja':'Translate to Japanese:',
|
||||
'ko':'Translate to Korean:',
|
||||
}
|
||||
const userPrompt = [
|
||||
`[Target Language: ${targetLanguage}]
|
||||
IMPORTANT: Your previous answer violated language rules.`,
|
||||
USER_PROMPTS_HINT[dto.target_lang],
|
||||
dto.text
|
||||
].filter(Boolean).join('\n');
|
||||
|
||||
if(!dto.target_model) {
|
||||
// 🧭 Smart routing
|
||||
const decision = this.router.route({
|
||||
language: dto.target_lang,
|
||||
contentType: 'translation',
|
||||
});
|
||||
dto.target_model = decision.writer;
|
||||
}
|
||||
const provider = this.factory.get(dto.target_model);
|
||||
const draft = await provider.complete(
|
||||
[
|
||||
{role: 'system', content: systemPrompt},
|
||||
{role: 'user', content: userPrompt},
|
||||
],
|
||||
{ temperature: 0.1},
|
||||
);
|
||||
this.logger.debug(`===> ${draft.model} đã dich xong!`);
|
||||
this.logger.debug(`===> ${draft.content}`);
|
||||
|
||||
return draft;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user