345 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
文字内容服务层
封装现有功能提供API调用
"""
import logging
import uuid
from typing import List, Dict, Any, Optional, Tuple
from datetime import datetime
from core.config import ConfigManager, GenerateTopicConfig, GenerateContentConfig
from core.ai import AIAgent
from utils.file_io import OutputManager
from tweet.topic_generator import TopicGenerator
from tweet.content_generator import ContentGenerator
from tweet.content_judger import ContentJudger
from api.services.prompt_builder import PromptBuilderService
from api.services.prompt_service import PromptService
logger = logging.getLogger(__name__)
class TweetService:
"""文字内容服务类"""
def __init__(self, ai_agent: AIAgent, config_manager: ConfigManager, output_manager: OutputManager):
"""
初始化文字内容服务
Args:
ai_agent: AI代理
config_manager: 配置管理器
output_manager: 输出管理器
"""
self.ai_agent = ai_agent
self.config_manager = config_manager
self.output_manager = output_manager
# 初始化各个组件
self.topic_generator = TopicGenerator(ai_agent, config_manager, output_manager)
self.content_generator = ContentGenerator(ai_agent, config_manager, output_manager)
self.content_judger = ContentJudger(ai_agent, config_manager, output_manager)
# 初始化提示词服务和构建器
self.prompt_service = PromptService(config_manager)
self.prompt_builder = PromptBuilderService(config_manager, self.prompt_service)
async def generate_topics(self, dates: Optional[str] = None, numTopics: int = 5,
styles: Optional[List[str]] = None,
audiences: Optional[List[str]] = None,
scenic_spots: Optional[List[str]] = None,
products: Optional[List[str]] = None) -> Tuple[str, List[Dict[str, Any]]]:
"""
生成选题
Args:
dates: 日期字符串,可能为单个日期、多个日期用逗号分隔或范围
numTopics: 要生成的选题数量
styles: 风格列表
audiences: 受众列表
scenic_spots: 景区列表
products: 产品列表
Returns:
请求ID和生成的选题列表
"""
logger.info(f"开始生成选题,日期: {dates}, 数量: {numTopics}")
# 获取并更新配置
topic_config = self.config_manager.get_config('topic_gen', GenerateTopicConfig)
if dates:
topic_config.topic.date = dates
topic_config.topic.num = numTopics
# 使用PromptBuilderService构建提示词
system_prompt, user_prompt = self.prompt_builder.build_topic_prompt(
products=products,
scenic_spots=scenic_spots,
styles=styles,
audiences=audiences,
dates=dates,
numTopics=numTopics
)
# 使用预构建的提示词生成选题
topics = await self.topic_generator.generate_topics_with_prompt(system_prompt, user_prompt)
if not topics:
logger.error("未能生成任何选题")
return str(uuid.uuid4()), []
# 生成请求ID
requestId = f"topic-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{str(uuid.uuid4())[:8]}"
logger.info(f"选题生成完成请求ID: {requestId}, 数量: {len(topics)}")
return requestId, topics
async def generate_content(self, topic: Optional[Dict[str, Any]] = None,
styles: Optional[List[str]] = None,
audiences: Optional[List[str]] = None,
scenic_spots: Optional[List[str]] = None,
products: Optional[List[str]] = None,
autoJudge: bool = False) -> Tuple[str, str, Dict[str, Any]]:
"""
为选题生成内容
Args:
topic: 选题信息
styles: 风格列表
audiences: 受众列表
scenic_spots: 景区列表
products: 产品列表
autoJudge: 是否自动进行内容审核
Returns:
请求ID、选题索引和生成的内容包含judgeSuccess状态
"""
# 如果没有提供topic创建一个基础的topic
if not topic:
topic = {"index": "1", "date": "2024-07-01"}
topicIndex = topic.get('index', 'N/A')
logger.info(f"开始为选题 {topicIndex} 生成内容{'(含审核)' if autoJudge else ''}")
# 创建topic的副本并应用覆盖参数
enhanced_topic = topic.copy()
if styles and len(styles) > 0:
enhanced_topic['style'] = styles[0] # 使用第一个风格
if audiences and len(audiences) > 0:
enhanced_topic['targetAudience'] = audiences[0] # 使用第一个受众
if scenic_spots and len(scenic_spots) > 0:
enhanced_topic['object'] = scenic_spots[0] # 使用第一个景区
if products and len(products) > 0:
enhanced_topic['product'] = products[0] # 使用第一个产品
# 使用PromptBuilderService构建提示词
system_prompt, user_prompt = self.prompt_builder.build_content_prompt(enhanced_topic, "content")
# 使用预构建的提示词生成内容
content = await self.content_generator.generate_content_with_prompt(enhanced_topic, system_prompt, user_prompt)
if not content:
logger.error(f"未能为选题 {topicIndex} 生成内容")
return str(uuid.uuid4()), topicIndex, {}
# 如果启用自动审核,进行内嵌审核
if autoJudge:
try:
logger.info(f"开始对选题 {topicIndex} 的内容进行内嵌审核")
# 使用PromptBuilderService构建审核提示词
judge_system_prompt, judge_user_prompt = self.prompt_builder.build_judge_prompt(enhanced_topic, content)
# 进行内容审核
judged_content = await self.content_judger.judge_content_with_prompt(content, enhanced_topic, judge_system_prompt, judge_user_prompt)
# 统一输出格式始终包含judgeSuccess状态
# content_judger返回的是judge_success字段下划线命名
judge_success = judged_content.get('judge_success', False)
if judge_success:
logger.info(f"选题 {topicIndex} 内容审核成功")
# 审核成功使用审核后的内容但移除judge_success添加统一的judgeSuccess
content = {k: v for k, v in judged_content.items() if k != 'judge_success'}
content['judgeSuccess'] = True
else:
logger.warning(f"选题 {topicIndex} 内容审核失败,保持原始内容")
# 审核失败保持原始内容添加judgeSuccess=False标记
content['judgeSuccess'] = False
except Exception as e:
logger.error(f"选题 {topicIndex} 内嵌审核失败: {e},保持原始内容")
# 审核异常保持原始内容添加judgeSuccess=False标记
content['judgeSuccess'] = False
else:
# 未启用审核添加judgeSuccess=None标记表示未进行审核
content['judgeSuccess'] = None
# 生成请求ID
requestId = f"content-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{str(uuid.uuid4())[:8]}"
logger.info(f"内容生成完成请求ID: {requestId}, 选题索引: {topicIndex}")
return requestId, topicIndex, content
async def generate_content_with_prompt(self, topic: Dict[str, Any], system_prompt: str, user_prompt: str) -> Tuple[str, str, Dict[str, Any]]:
"""
使用预构建的提示词为选题生成内容
Args:
topic: 选题信息
system_prompt: 系统提示词
user_prompt: 用户提示词
Returns:
请求ID、选题索引和生成的内容
"""
topicIndex = topic.get('index', 'unknown')
logger.info(f"开始使用预构建提示词为选题 {topicIndex} 生成内容")
# 直接使用预构建的提示词生成内容
content = await self.content_generator.generate_content_with_prompt(topic, system_prompt, user_prompt)
# 生成请求ID
requestId = f"content-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{str(uuid.uuid4())[:8]}"
logger.info(f"内容生成完成请求ID: {requestId}, 选题索引: {topicIndex}")
return requestId, topicIndex, content
async def judge_content(self, topic: Optional[Dict[str, Any]] = None, content: Dict[str, Any] = {},
styles: Optional[List[str]] = None,
audiences: Optional[List[str]] = None,
scenic_spots: Optional[List[str]] = None,
products: Optional[List[str]] = None) -> Tuple[str, str, Dict[str, Any], bool]:
"""
审核内容
Args:
topic: 选题信息
content: 要审核的内容
styles: 风格列表
audiences: 受众列表
scenic_spots: 景区列表
products: 产品列表
Returns:
请求ID、选题索引、审核后的内容和审核是否成功
"""
# 如果没有提供topic创建一个基础的topic
if not topic:
topic = {"index": "1", "date": "2024-07-01"}
# 如果没有提供content返回错误
if not content:
content = {"title": "未提供内容", "content": "未提供内容"}
topicIndex = topic.get('index', 'unknown')
logger.info(f"开始审核选题 {topicIndex} 的内容")
# 创建topic的副本并应用覆盖参数
enhanced_topic = topic.copy()
if styles and len(styles) > 0:
enhanced_topic['style'] = styles[0] # 使用第一个风格
if audiences and len(audiences) > 0:
enhanced_topic['targetAudience'] = audiences[0] # 使用第一个受众
if scenic_spots and len(scenic_spots) > 0:
enhanced_topic['object'] = scenic_spots[0] # 使用第一个景区
if products and len(products) > 0:
enhanced_topic['product'] = products[0] # 使用第一个产品
# 使用PromptBuilderService构建提示词
system_prompt, user_prompt = self.prompt_builder.build_judge_prompt(enhanced_topic, content)
# 使用预构建的提示词进行审核
judged_data = await self.content_judger.judge_content_with_prompt(content, enhanced_topic, system_prompt, user_prompt)
# 提取审核是否成功content_judger返回judge_success字段
judgeSuccess = judged_data.get('judge_success', False)
# 统一字段命名移除judge_success添加judgeSuccess
if 'judge_success' in judged_data:
judged_data = {k: v for k, v in judged_data.items() if k != 'judge_success'}
judged_data['judgeSuccess'] = judgeSuccess
# 生成请求ID
requestId = f"judge-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{str(uuid.uuid4())[:8]}"
logger.info(f"内容审核完成请求ID: {requestId}, 选题索引: {topicIndex}, 审核结果: {judgeSuccess}")
return requestId, topicIndex, judged_data, judgeSuccess
async def run_pipeline(self, dates: Optional[str] = None, numTopics: int = 5,
styles: Optional[List[str]] = None,
audiences: Optional[List[str]] = None,
scenic_spots: Optional[List[str]] = None,
products: Optional[List[str]] = None,
skipJudge: bool = False,
autoJudge: bool = False) -> Tuple[str, List[Dict[str, Any]], Dict[str, Dict[str, Any]], Dict[str, Dict[str, Any]]]:
"""
运行完整的内容生成流水线:生成选题 → 生成内容 → 审核内容
Args:
dates: 日期字符串,可能为单个日期、多个日期用逗号分隔或范围
numTopics: 要生成的选题数量
styles: 风格列表
audiences: 受众列表
scenic_spots: 景区列表
products: 产品列表
skipJudge: 是否跳过内容审核步骤与autoJudge互斥
autoJudge: 是否在内容生成时进行内嵌审核
Returns:
请求ID、选题列表、内容字典和审核后内容字典
"""
logger.info(f"开始运行完整流水线,日期: {dates}, 数量: {numTopics}, 内嵌审核: {autoJudge}")
# 生成请求ID
requestId = f"pipeline-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{str(uuid.uuid4())[:8]}"
# 1. 生成选题
_, topics = await self.generate_topics(dates, numTopics, styles, audiences, scenic_spots, products)
if not topics:
logger.error("未能生成任何选题")
return requestId, [], {}, {}
# 2. 为每个选题生成内容
contents = {}
judgedContents = {}
for topic in topics:
topicIndex = topic.get('index', 'unknown')
_, _, content = await self.generate_content(topic, autoJudge=autoJudge)
if autoJudge:
# 内嵌审核模式content已包含审核结果和judgeSuccess状态
# 创建原始内容副本移除judgeSuccess状态但保留其他字段
original_content = {k: v for k, v in content.items() if k != 'judgeSuccess'}
contents[topicIndex] = original_content
judgedContents[topicIndex] = content # 包含审核结果和judgeSuccess状态
else:
# 无审核模式:直接保存内容
contents[topicIndex] = content
# 如果使用内嵌审核或跳过审核,直接返回结果
if autoJudge or skipJudge:
logger.info(f"{'使用内嵌审核' if autoJudge else '跳过内容审核步骤'}流水线完成请求ID: {requestId}")
if autoJudge:
return requestId, topics, contents, judgedContents
else:
return requestId, topics, contents, contents
# 3. 对每个内容进行审核
judgedContents = {}
for topicIndex, content in contents.items():
topic = next((t for t in topics if t.get('index') == topicIndex), None)
if not topic:
logger.warning(f"找不到选题 {topicIndex} 的原始数据,跳过审核")
continue
try:
_, _, judged_data, _ = await self.judge_content(topic, content)
judgedContents[topicIndex] = judged_data
except Exception as e:
logger.critical(f"为选题 {topicIndex} 处理内容审核时发生意外错误: {e}", exc_info=True)
logger.info(f"流水线完成请求ID: {requestId}")
return requestId, topics, contents, judgedContents