|
|
@@ -9,10 +9,11 @@ import sys
|
|
|
from dotenv import load_dotenv
|
|
|
from pydantic import BaseModel
|
|
|
from src.models.product_model import (
|
|
|
- Product, CompetitorCrawlData, AICompetitorAnalyzeMainKeywords,
|
|
|
+ AIAnalyzeCompare, Product, CompetitorCrawlData, AICompetitorAnalyzeMainKeywords,
|
|
|
TrafficKeywordResult, ProductImageInfo,AICompetitorAnalyzeMainKeywordsResult,
|
|
|
- SearchAmazoneKeyResult, ProductBaseInfo, Variant
|
|
|
+ SearchAmazoneKeyResult, ProductBaseInfo, Variant,MarketingInfo,
|
|
|
)
|
|
|
+from src.models.config_model import (UserConfig, AIPromptConfig, )
|
|
|
from llama_index.llms.openai import OpenAI
|
|
|
from llama_index.llms.litellm import LiteLLM
|
|
|
from src.manager.core.db_mongo import BaseMongoManager
|
|
|
@@ -86,6 +87,48 @@ def get_competitor_prompt_data(
|
|
|
|
|
|
return list_data
|
|
|
|
|
|
+class PromptFormatter:
|
|
|
+ """LLM提示词模板格式化器"""
|
|
|
+ def __init__(self, template: str, **kwargs):
|
|
|
+ self.template = template
|
|
|
+ self.kwargs = kwargs
|
|
|
+ self.partial_kwargs = {}
|
|
|
+ self.var_mappings = {}
|
|
|
+ self.function_mappings = {}
|
|
|
+
|
|
|
+ def partial_format(self, **kwargs) -> "PromptFormatter":
|
|
|
+ """部分格式化模板"""
|
|
|
+ self.partial_kwargs.update(kwargs)
|
|
|
+ return self
|
|
|
+
|
|
|
+ def map_variables(self, **mappings) -> "PromptFormatter":
|
|
|
+ """映射模板变量名"""
|
|
|
+ self.var_mappings.update(mappings)
|
|
|
+ return self
|
|
|
+
|
|
|
+ def map_functions(self, **functions) -> "PromptFormatter":
|
|
|
+ """映射模板处理函数"""
|
|
|
+ self.function_mappings.update(functions)
|
|
|
+ return self
|
|
|
+
|
|
|
+ def format(self, **kwargs) -> str:
|
|
|
+ """最终格式化提示词"""
|
|
|
+ # 合并所有参数
|
|
|
+ all_kwargs = {**self.partial_kwargs, **kwargs}
|
|
|
+
|
|
|
+ # 应用变量名映射
|
|
|
+ mapped_kwargs = {}
|
|
|
+ for key, value in all_kwargs.items():
|
|
|
+ mapped_key = self.var_mappings.get(key, key)
|
|
|
+ mapped_kwargs[mapped_key] = value
|
|
|
+
|
|
|
+ # 应用函数处理
|
|
|
+ for key, func in self.function_mappings.items():
|
|
|
+ if key in mapped_kwargs:
|
|
|
+ mapped_kwargs[key] = func(**mapped_kwargs)
|
|
|
+
|
|
|
+ return self.template.format(**mapped_kwargs)
|
|
|
+
|
|
|
class Formatter(ABC):
|
|
|
"""格式化器抽象基类"""
|
|
|
def __init__(self, notes: Optional[dict] = None):
|
|
|
@@ -151,9 +194,10 @@ class LiteLLMService(LLMService):
|
|
|
async def analyze(self, prompt: str) -> Union[dict, str]:
|
|
|
llm_kwargs = {}
|
|
|
if self.format_type == "json":
|
|
|
- if 'deepseek-r' not in self.model:
|
|
|
- llm_kwargs["additional_kwargs"] = {"response_format": {"type": "json_object"}}
|
|
|
-
|
|
|
+ # if 'deepseek-r' not in self.model:
|
|
|
+ # llm_kwargs["additional_kwargs"] = {"response_format": {"type": "json_object"}}
|
|
|
+ prompt += "\n请确保输出的是有效的JSON对象。"
|
|
|
+ logger.info(f"{self.model} 调用参数: {llm_kwargs}")
|
|
|
llm = LiteLLM(model=self.model, **llm_kwargs)
|
|
|
|
|
|
for attempt in range(self.max_retries):
|
|
|
@@ -169,7 +213,7 @@ class LiteLLMService(LLMService):
|
|
|
else:
|
|
|
raise ValueError(f"无法获取有效的JSON响应: {str(e)}")
|
|
|
except Exception as e:
|
|
|
- logger.error(f"LLM调用失败: {str(e)}")
|
|
|
+ logger.exception(f"LLM调用失败: {str(e)}")
|
|
|
raise
|
|
|
|
|
|
def _process_response(self, response_text: str) -> Union[dict, str]:
|
|
|
@@ -193,12 +237,125 @@ class AnalysisService:
|
|
|
self.llm_service:LiteLLMService = llm_service
|
|
|
self.db_manager = db_manager
|
|
|
|
|
|
- async def execute_analysis(self, product:Product, format_type: str = "json", dry_run=False) -> tuple[dict, str]:
|
|
|
- prompt = await self._prepare_prompt(product, format_type)
|
|
|
- logger.info(f"prompt: {prompt}")
|
|
|
+ async def execute_analysis(self, product:Product, format_type: str = "json", dry_run=False, template: Optional[AIPromptConfig] = None) -> tuple[dict, str]:
|
|
|
+ # if template:
|
|
|
+ # formatter = PromptFormatter(template.template)
|
|
|
+ # if template.keywords:
|
|
|
+ # formatter.partial_format(**template.keywords)
|
|
|
+ # prompt = formatter.format(
|
|
|
+ # product=product,
|
|
|
+ # format_type=format_type
|
|
|
+ # )
|
|
|
+ # else:
|
|
|
+ # prompt = await self._prepare_prompt(product, format_type)
|
|
|
+
|
|
|
+ # logger.info(f"prompt: {prompt}")
|
|
|
+ # analysis_result = await self.llm_service.analyze(prompt)
|
|
|
+ # return analysis_result, prompt
|
|
|
+ pass
|
|
|
+
|
|
|
+ async def execute_marketing_analysis(self, product: Product, format_type: str = "json", template: Optional[AIPromptConfig] = None) -> tuple[MarketingInfo, str]:
|
|
|
+ """
|
|
|
+ 执行营销文案分析
|
|
|
+
|
|
|
+ Args:
|
|
|
+ product: 产品对象
|
|
|
+ format_type: 输出格式
|
|
|
+ template: 自定义提示模板
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ (分析结果, 使用的提示词)
|
|
|
+ """
|
|
|
+ if template:
|
|
|
+ formatter = PromptFormatter(template.template)
|
|
|
+ if template.keywords:
|
|
|
+ formatter.partial_format(**template.keywords)
|
|
|
+ prompt = formatter.format(
|
|
|
+ product=product,
|
|
|
+ format_type=format_type
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ prompt = f'''我是亚马逊运营,请为产品 {product.basic_info.name} 生成营销文案。
|
|
|
+
|
|
|
+产品信息:
|
|
|
+{product.basic_info.model_dump_json(indent=2)}
|
|
|
+
|
|
|
+要求:
|
|
|
+- 突出产品卖点: {', '.join(product.basic_info.selling_point)}
|
|
|
+- 适合日本市场风格
|
|
|
+- 包含吸引人的标题和详细描述'''
|
|
|
+
|
|
|
+ logger.info(f"营销分析提示词: {prompt}")
|
|
|
analysis_result = await self.llm_service.analyze(prompt)
|
|
|
- return analysis_result, prompt
|
|
|
+
|
|
|
+ try:
|
|
|
+ marketing_info = MarketingInfo(**analysis_result)
|
|
|
+ return marketing_info, prompt
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"营销分析结果解析失败: {str(e)}")
|
|
|
+ raise ValueError("营销分析结果格式不正确") from e
|
|
|
|
|
|
+ async def _prepare_competitor_prompt(self, product: Product, template: AIPromptConfig) -> str:
|
|
|
+ """使用llmaindex模板方法格式化提示词
|
|
|
+
|
|
|
+ Args:
|
|
|
+ product: 产品对象
|
|
|
+ template: 提示模板配置
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ 格式化后的提示词字符串
|
|
|
+ """
|
|
|
+ output_fields = get_field_descriptions(
|
|
|
+ AICompetitorAnalyzeMainKeywordsResult,
|
|
|
+ exclude=['results.crawl_result', 'results.created_at']
|
|
|
+ )
|
|
|
+ formatter = FormatterFactory.create_formatter(self.llm_service.format_type)
|
|
|
+ output_format = formatter.format(output_fields)
|
|
|
+
|
|
|
+ competitor_data = get_competitor_prompt_data(product)
|
|
|
+ basic_template =f'''各个字段说明:
|
|
|
+{get_field_descriptions(CompetitorCrawlData, include=['asin'])}
|
|
|
+{get_field_descriptions(ProductImageInfo, include=['main_text'])}
|
|
|
+{get_field_descriptions(TrafficKeywordResult, include=['traffic_keyword', 'monthly_searches'])}
|
|
|
+
|
|
|
+竞品数据:
|
|
|
+{competitor_data}
|
|
|
+
|
|
|
+我的产品信息如下:
|
|
|
+{product.basic_info.model_dump_json(indent=2)}
|
|
|
+
|
|
|
+返回格式:
|
|
|
+{output_format}
|
|
|
+----
|
|
|
+'''
|
|
|
+ template.template = basic_template + template.template
|
|
|
+ return template.template
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def convert_monthly_searches(value):
|
|
|
+ if value is None:
|
|
|
+ return None
|
|
|
+ if isinstance(value, str):
|
|
|
+ if not value.strip():
|
|
|
+ return None
|
|
|
+ return int(value.replace(',', ''))
|
|
|
+ return value
|
|
|
+ async def run_competitor_analysis(self, product: Product,
|
|
|
+ ai_analyze_compare_model: AIAnalyzeCompare,
|
|
|
+ format_type: str = 'json',):
|
|
|
+ prompt = await self._prepare_competitor_prompt(product, ai_analyze_compare_model.competitor_template)
|
|
|
+ logger.info(f"_prepare_competitor_prompt {prompt}")
|
|
|
+ analyze_result = await self.llm_service.analyze(prompt)
|
|
|
+ if 'results' in analyze_result:
|
|
|
+ for result in analyze_result['results']:
|
|
|
+ if 'monthly_searches' in result:
|
|
|
+ result['monthly_searches'] = self.convert_monthly_searches(result['monthly_searches'])
|
|
|
+
|
|
|
+ if 'tail_keys' in analyze_result:
|
|
|
+ for tail_key in analyze_result['tail_keys']:
|
|
|
+ if 'monthly_searches' in tail_key:
|
|
|
+ tail_key['monthly_searches'] = self.convert_monthly_searches(tail_key['monthly_searches'])
|
|
|
+ return analyze_result
|
|
|
async def _prepare_prompt(self, product: Product, format_type: str = "json", main_key_num: int = 3, tail_key_num:int = 12) -> str:
|
|
|
competitor_data = get_competitor_prompt_data(product)
|
|
|
# 从数据模型获取输出字段描述
|
|
|
@@ -216,9 +373,11 @@ class AnalysisService:
|
|
|
|
|
|
竞品数据:
|
|
|
{competitor_data}
|
|
|
-----
|
|
|
-我是日本站的亚马逊运营,我在给产品名称为 {product.basic_info.name} 选主要关键词,我的产品信息如下:
|
|
|
+
|
|
|
+我的产品信息如下:
|
|
|
{product.basic_info.model_dump_json(indent=2)}
|
|
|
+----
|
|
|
+我是日本站的亚马逊运营,我在给产品名称为 {product.basic_info.name} 选主要关键词和长尾关键词。
|
|
|
|
|
|
请根据以上 {len(competitor_data)} 个竞品数据,按以下规则分析:
|
|
|
- 选出搜索量在1万以上的相同关键词作为主要关键词{main_key_num}个。
|