mrh 6 mesi fa
parent
commit
be5a78824b

+ 9 - 2
ai/base_agent.py

@@ -1,3 +1,4 @@
+import re
 from llama_index.llms.litellm import LiteLLM
 from src.manager.template_manager import TemplateManager, TemplateService, TemplateType
 
@@ -6,5 +7,11 @@ class BaseAgent:
         self.llm = llm
         self.template_manager = template_manager
     
-    def get_mainkeys_tailkeys(self, template_str: str):
-        pass
+    def filter_markdown_content(self, llm_str: str):
+        pattern = r'```markdown(.*?)```'
+        matches = re.findall(pattern, llm_str, re.DOTALL)
+        if not matches:
+            markdown = llm_str
+        else:
+            markdown = matches[0]
+        return markdown

+ 19 - 0
ai/demo/sigle_flow.yaml

@@ -0,0 +1,19 @@
+name: QuickStart
+
+control-plane:
+  port: 8000
+
+default-service: echo_workflow
+
+services:
+  echo_workflow:
+    name: Echo Workflow
+    # We tell LlamaDeploy where to look for our workflow
+    source:
+      # In this case, we instruct LlamaDeploy to look in the local filesystem
+      type: local
+      # The path in the local filesystem where to look. This assumes there's an src folder in the
+      # current working directory containing the file workflow.py we created previously
+      name: ./
+    # This assumes the file workflow.py contains a variable called `echo_workflow` containing our workflow instance
+    path: single_flow:echo_workflow

+ 24 - 0
ai/demo/single_flow.py

@@ -0,0 +1,24 @@
+import asyncio
+from llama_index.core.workflow import Workflow, StartEvent, StopEvent, step
+
+
+class EchoWorkflow(Workflow):
+    """A dummy workflow with only one step sending back the input given."""
+
+    @step()
+    async def run_step(self, ev: StartEvent) -> StopEvent:
+        message = str(ev.get("message", ""))
+        return StopEvent(result=f"Message received: {message}")
+
+
+# `echo_workflow` will be imported by LlamaDeploy
+echo_workflow = EchoWorkflow()
+
+
+async def main():
+    print(await echo_workflow.run(message="Hello!"))
+
+
+# Make this script runnable from the shell so we can test the workflow execution
+if __name__ == "__main__":
+    asyncio.run(main())

+ 27 - 0
ai/load_template.py

@@ -0,0 +1,27 @@
+from llama_index.readers.file import PandasExcelReader
+from llama_index.readers.docling import DoclingReader
+from llama_index.core.node_parser import MarkdownNodeParser
+from pathlib import Path
+'''
+https://docs.llamaindex.ai/en/stable/examples/data_connectors/DoclingReaderDemo/
+uv pip install llama-index-readers-docling llama-index-node-parser-docling llama-index-readers-file
+'''
+
+def read_docling():
+    reader = DoclingReader(export_type=DoclingReader.ExportType.MARKDOWN, )
+    res = reader.load_data(Path(r"C:\Users\mg\Downloads\塑料园艺多肉三件套-文案制作模版.xlsx"))
+    print(res[0].text)
+    with open("docling.md", "w", encoding="utf-8") as f:
+        f.write(res[0].text)
+
+def read_excel(file_path):
+
+    reader = PandasExcelReader()
+    res = reader.load_data(file=r"C:\Users\mg\Downloads\塑料园艺多肉三件套-文案制作模版.xlsx")
+    print(res)
+
+def main():
+    read_docling()
+
+if __name__ == "__main__":
+    main()

+ 23 - 34
ai/marketting_agent.py

@@ -41,6 +41,7 @@ class MarketingAgent(BaseAgent):
 - 如果竞品的搜索量都不足1万,则从排名前十的关键词中筛选3个搜索量最大且相关性最强的词。
 - 结合日本市场特点分析
 - 根据我的产品基本信息,从竞品的主要信息和同类竞品的相似关键词中,筛选出最符合我产品的长尾关键词 10个以上
+- 如果涉及品牌名,不要将品牌名作为关键词,因为那是竞品关键词参考数据,不是我的产品品牌。
 
 
 筛选长尾词的示例:
@@ -49,7 +50,7 @@ class MarketingAgent(BaseAgent):
 
 生成的内容满足以下要求:
 - reason 、 suggestions 必须写成中文
-- monthly_searches 必须是 int ,按照从大到小排序,别的字段按照源数据填写即可。
+- 月搜索量 monthly_searches 必须是 int ,按照从大到小排序,别的字段按照源数据填写即可。
 - 内容格式必须是 {output_type} 
 '''
         variables = {'product_name': product_name}
@@ -86,12 +87,7 @@ class MarketingAgent(BaseAgent):
             competitor = self.llm_mainkeys_tailkeys_to_model(prompt_template, verbose)
         elif output_type == 'markdown':
             response = await self.llm.acomplete(prompt_template.format())
-            pattern = r'```markdown(.*?)```'
-            matches = re.findall(pattern, response.text, re.DOTALL)
-            if not matches:
-                competitor = response.text
-            else:
-                competitor = matches[0]
+            competitor = self.filter_markdown_content(response.text)
         agent_model.product = await Product.find_one(Product.basic_info.name == product_name)
         agent_model.competitor[output_type] = competitor
         agent_model.update_time = datetime.now()
@@ -99,22 +95,7 @@ class MarketingAgent(BaseAgent):
 
     async def get_marketing_prompt(self, product_name, prompt: str='', verbose=False, llm=None):
         prompt_marketing = prompt or '''\
-你是日本站的亚马逊运营,请你根据产品信息、竞品相似主关键词、竞品长尾词,为用户生成营销文案和5个卖点文案,需要用日文。
 
-请根据竞品数据,按以下规则分析:
-- 选出搜索量在1万以上的相同关键词作为主要关键词3个。
-- 如果竞品的搜索量都不足1万,则从排名前十的关键词中筛选3个搜索量最大且相关性最强的词。
-- 结合日本市场特点分析
-- 根据我的产品基本信息,从竞品的主要信息和同类竞品的相似关键词中,筛选出最符合我产品的长尾关键词 10个以上
-
-
-筛选长尾词的示例:
-- 假设我的产品是电线保护,那么竞品关键词中,“隐藏排线管” 就不符合长尾词
-- 假设我的产品是“防老化、防动物咬”用途,你就不能在竞品数据中选择不属于我这个使用场景的长尾关键词。
-
-生成的内容满足以下要求:
-- reason 、 suggestions 必须写成中文
-- monthly_searches 必须是 int ,按照从大到小排序,别的字段按照源数据填写即可。
 '''
         all_keywords = self.template_manager.execute_template('agent.mainkeys_tailkeys')
         variables = {'product_name': product_name}
@@ -181,16 +162,15 @@ async def llm_task(product_name):
     # model = 'openai/deepseek-v3'
     # model = 'openai/groq/qwen-2.5-32b'
     # model = 'openai/deepseek-chat'
+    # 'openai/QwQ-32B',
     model = 'openai/deepseek-reasoner'
     # model = 'openai/doubao-pro-32k-241215'
     llm_list = [
-        # 'openai/doubao-pro-32k-241215',
         LiteLLM(model='openai/doubao-pro-32k-241215', api_key=OPENAI_API_KEY, api_base=OPENAI_API_BASE),
-        # 'openai/deepseek-reasoner',
+        LiteLLM(model='openai/Qwen/Qwen3-235B-A22B', api_key=OPENAI_API_KEY, api_base=OPENAI_API_BASE),
         LiteLLM(model='openai/deepseek-reasoner', api_key=OPENAI_API_KEY, api_base=OPENAI_API_BASE),
-        # 'openai/deepseek-v3',
-        LiteLLM(model='openai/deepseek-v3', api_key=OPENAI_API_KEY, api_base=OPENAI_API_BASE),
-        # 'openai/QwQ-32B',
+        # LiteLLM(model='openai/deepseek-v3', api_key=OPENAI_API_KEY, api_base=OPENAI_API_BASE),
+        
     ]
     task_list = []
     for llm in llm_list:
@@ -211,19 +191,28 @@ async def gen_marketing_file(product_name):
     llm = LiteLLM(model=model, api_key=OPENAI_API_KEY, api_base=OPENAI_API_BASE)
     # product_name = '大尺寸厚款卸妆棉240片'
     agent = MarketingAgent(llm=llm, template_manager=m)
-    output_path = r'G:\code\amazone\copywriting_production\output\temp' + f"\\{product_name}-营销文案.md"
+    output_path = r'G:\code\amazone\copywriting_production\output\temp' + f"\\{product_name}-主关键词长尾词.md"
     llm_models = [
 'openai/doubao-pro-32k-241215',
 'openai/deepseek-reasoner',
-'openai/deepseek-v3', 
+# 'openai/deepseek-v3', 
 # 'openai/QwQ-32B',
  ]
     await agent.gen_marketing_file(product_name=product_name, output_path=output_path, llm_models=llm_models)
     logger.info(f"{output_path}")
-def main():
-    product_name = '狗刷牙指套'
-    # product_name = '园艺镊子套装2件套'
-    asyncio.run(llm_task(product_name=product_name))
+async def main():
+    list_product_name = [
+        # '珍珠纹卸妆棉片',
+        '镊子铲两件套',
+        '折叠剪刀',
+        '脸用刮毛刀4件套',
+        '路亚钓鱼小剪刀',
+    ]
+    task_list = []
+    for product_name in list_product_name:
+        task_list.append(llm_task(product_name))
+    await asyncio.gather(*task_list)
+    # asyncio.run(llm_task(product_name=product_name))
 
 if __name__ == "__main__":
-    main()
+    asyncio.run(main())

+ 0 - 3
ai/product_agent.py

@@ -1,3 +0,0 @@
-from ai.base_agent import BaseAgent
-from llama_index.core.prompts import PromptTemplate
-from llama_index.llms.litellm import LiteLLM

+ 0 - 0
ai/product_gpt_prompt.py


+ 309 - 0
ai/product_title_agent.py

@@ -0,0 +1,309 @@
+import asyncio
+from datetime import datetime
+import re
+import aiofiles
+import os
+import sys
+from typing import List, Dict, Any
+from typing import Any, Dict, List, Optional, Union
+from pydantic import BaseModel
+from typing import List
+from llama_index.core.program import LLMTextCompletionProgram
+from llama_index.core.output_parsers import PydanticOutputParser
+from llama_index.program.lmformatenforcer import (
+    LMFormatEnforcerPydanticProgram,
+)
+from llama_index.core.prompts import PromptTemplate
+from llama_index.llms.llama_cpp import LlamaCPP
+from llama_index.llms.openai import OpenAI
+from llama_index.llms.litellm import LiteLLM
+from llama_index.core.llms.llm import LLM
+from src.models.product_model import Product
+from src.manager.template_manager import TemplateManager, TemplateService, TemplateType
+from src.models.ai_execution_record import MarketingInfo, LLMConfig, SuperPromptMixin, AgentConfig, AgentContent, AICompetitorAnalyzeMainKeywords, AICompetitorAnalyzeMainKeywordsResult, MarketingContentGeneration
+from ai.base_agent import BaseAgent
+from llama_index.core.prompts import PromptTemplate
+from llama_index.llms.litellm import LiteLLM
+from config.settings import MONGO_URL, MONGO_DB_NAME,LITELLM_API_BASE, LITELLM_API_KEY,OPENAI_API_KEY,OPENAI_API_BASE
+from utils.file import save_to_file, read_file
+from utils.logu import get_logger
+
+logger = get_logger("ai")
+
+class ProductTitleAgent(BaseAgent):
+    async def get_promp(self, product_name, prompt: str='', prompt_keywords:Dict={}) -> PromptTemplate:
+        context_template = '''\
+这是我的产品信息:
+```\n{product_info}\n```\n
+这是我根据自己产品信息,参考同行竞品的主关键词和长尾关键词:
+```\n{llm_keyword_markdown}\n```\n
+---
+'''
+        prompt_product = prompt or '''\
+### 必须满足以下要求
+#### 平台要求:
+亚马逊商品要点(Bullet Point)也就是卖家常说的五点描述,五点描述重点了解商品的五个主要特征和优势,帮助买家快速确认商品是否适合自己。  
+
+要点  
+
+这些内容应等同于商品的“功能与优点”。建议您为 5 个要点采用以下格式:  
+
+特性要点 1: 内容(例如材质)  
+特性要点 2: 功能与优点  
+特性要点 3: 功能与优点  
+特性要点 4: 功能与优点  
+特性要点 5: 护理或特殊护理或保修  
+
+示例:  
+
+由 \(100 \%\) 柔软磨毛棉制成  
+适合周末休闲穿着的舒适衬衫  
+抗皱性强,无需熨烫  
+做旧水洗工艺让这款衬衫呈现出一种“穿过的”复古风格  
+温水机洗。滚筒干燥  
+
+常见的五点描述的例子包括:产品总体概要、尺寸 、 年龄适宜性、原产国、主要功能、材料和结构、保修信息  
+
+写作要求:  
+
+\(\textcircled{1}\) 以 大 写 字 母 开 头 。 例 如 : Cotton Fabric: Made from \(100 \%\) cotton for softness andbreathability  
+\(\textcircled{2}\) 采用句段形式,请勿使用末尾标点符号。例如:Long Sleeve: Long sleeves add coverageand style  
+\(\textcircled{3}\) 在一个要点中使用分号分隔各个短语。例如: Machine Washable: Durable construction;easier care  
+\(\textcircled{4}\) 使用10 个以上字符,但不超过255 个字符。  
+例如:Loose Fit: Relaxed fit allows for easy movement and comfort  
+解释:这里有59 个字符,包括50 个英文字母和每个词之间的空格,所以多过 10 个字符,且没有超过255 个字符  
+\(\textcircled{5}\) 写出数字1-9 的文字形式,但名称、型号和测量值除外。  
+例如:Long Battery Life: Total of twenty-four hours battery life  
+\(\textcircled{6}\) 为要点添加标题,并在标题后面使用“:”,然后再在后面提供完整信息。  
+例如:Machine Washable: Durable construction allows for easy care  
+\(\textcircled{7}\) 在数字和度量单位之间添加一个空格。  
+例如:...60 Milliliter…  
+\(\textcircled{8}\) 使用清晰、自然的语言编写要点,避免使用不必要的关键词或短语。  
+例如:可机洗:经久耐用,易于护理  
+\(\textcircled{9}\) 突出商品特征和优势,而不是品牌的营销活动。  
+反面示例:限时折扣:现在购买,享受 \(50 \%\) 折扣  
+\(\textcircled{10}\) 突出商品如何满足买家需求,而不只是列出事实。  
+例如:多用途款式:适合舞蹈练习、娱乐玩耍或户外活动  
+\(\textcircled{1}\) 在商品变体之间保持数据一致。  
+
+例如:  
+
+变体1 要点:棉织物:由 \(100 \%\) 纯棉制成,柔软、透气  
+变体2 要点:棉织物:由 \(100 \%\) 纯棉制成,柔软、透气  
+\(\textcircled{12}\) 请勿转述或引用此ASIN 不包含的其他商品。  
+反面示例:充电速度:本充电器属于快充,和苹果官方售卖的充电器的充电速度一样快\(\textcircled{13}\) 去除或尽可能减少商品名称、商品描述或商品概览等属性的重复。突出附加信息或支持信息,以帮助买家做出更明智的决策。  
+
+例如:  
+
+要点1:材质:这款水壶由优质不锈钢制成,持久耐用要点2:容量:水壶容量为1.5 升,适合家庭或户外使用要点3:保温:独特的双层保温设计,能够保持饮品温度长达12 小时解释:在正确的例子中,每个要点都提供了不同的产品信息(材料、容量和保温设计),这样客户可以更全面地了解产品的各个特点\(\textcircled{14}\) 避免使用任何主观声明、绩效声明或对比声明,除非它们可在商品包装上得到核实。  
+
+反面示例:…比X 品牌更耐用、更便宜,功能更多  
+\(\textcircled{15}\) 避免使用与荣誉和奖项有关的声明,除非商品详情页面提供有详细证明信息,例如日期和授予机构等。  
+反面示例:…ASIN 销量常年保持同类目第三  
+\(\textcircled{16}\) 避免使用与消费者调查结果有关的声明,即使相关调查收集了一些主观意见,除非提供有来源和日期。  
+反面示例:…根据调查, \(9 5 \%\) 的用户都推荐这款产品  
+
+写作禁止:  
+
+1.禁止使用特殊字符  
+
+例如™、®、€、…、†、‡、o、¢、£、¥、©、±、\~、â。  
+
+【错误示例】¥¥ Cotton Fabric ¥¥: Made from \(100 \%\) cotton for softness and breathability  
+
+2.禁止使用任何表情符号例如☺  
+
+【错误示例】 Cotton Fabric: Made from \(100 \%\) cotton for softness and breathability  
+
+3.禁止使用ASIN 编号或以下字样  
+例如“不适用” \(/ ^ { \prime \prime } \mathsf { N A ^ { \prime \prime } } / ^ { \prime \prime } \mathsf { n } / \mathsf { a ^ { \prime \prime } } / ^ { \prime \prime } \mathsf { N } / \mathsf { A ^ { \prime \prime } }\) 、“不符合要求”、“尚待决定”、“待决定”、“待定”、“待复制”。  
+
+【错误示例】Material:NA  
+
+4.禁止使用禁用短语例如生态友好、环境友好、抗微生物、抗菌、竹制、含竹类、大豆制成、含大豆。  
+
+5.禁止使用禁用短语例如生态友好、环境友好、抗微生物、抗菌、竹制、含竹类、大豆制成、含大豆。有关更多信息,您可复制以下链接至浏览器,前往参阅一般商品限制内容https://sellercentral.amazon.com/help/hub/reference/external/G201707070?locale \(\ c =\) en-US【错误示例】材质:本产品原材料是 \(100 \%\) 环保的,具有抗菌特性  
+
+6.禁用保证信息  
+
+例如“全额退款”、“如果不满意,请将其退回”或“无条件保证,无限制”。  
+
+【错误示例】…如果不满意,我们提供全额退款7.禁用公司信息、网站链接、外部超链接或任何联系信息【错误示例】限时优惠:欢迎访问我们的网站(www.example.com)获取更多额外优惠  
+
+8.禁用引导评论相关的信息  
+
+包括但不限于鼓励买家留下5 星好评,或者引导买家不留负面评论等。  
+
+【错误示例】…5 星好评免费赠送小礼物  
+
+9.禁用时效性信息例如宣传活动、研讨会或讲座的日期。  
+
+【错误示例】…欢迎参加9 月16 号xx 产品线下推广活动,地址位于…  
+
+10.禁止要点重复每个要点必须说明一条唯一的商品信息。  
+
+【错误示例】棉织物:由 \(100 \%\) 纯棉制成,柔软、透气;宽松合体:宽松合体便于运动且穿  
+着舒适;可机洗:经久耐用,易于护理  
+解释:示例里同时包含了3 条商品信息  
+
+
+#### 用户要求
+作为专业的日本站亚马逊运营,你需要根据产品信息和参考关键词,为我生成日语的产品标题、ST搜索词、卖点1~5。
+
+亚马逊标题要求:
+- 保持简洁:200 个字符是允许的最大字符数,因为手机屏幕会缩短较长的商品名称避免冗余:请勿在商品名称中包含冗余信息、不必要的同义词或过多的关键词优化词序:仅包含有助于买家快速识别和了解商品的信息,将词语排序以优先展示最重要的商品信息。
+- 创建商品名称时,请遵循以下标准: 
+  - 商品名称不能超过 200 个字符(包括空格)。
+  - 商品名称不可包含促销用语,如“ 無料配送”或“ 100% 品質保証、人気商品、ベストセラー 。” 
+  - 商品名称不能使用以下特殊字符:!、$、?、_、{、}、^、¬、¦。其他特殊字符,如 ~、#、<、> 和 *,只能在特定上下文中使用。
+    - 例如,您可以使用这些符号作为商品编码(“款式 #4301”)或测量值(“<10 磅”)。
+    - 不允许使用特殊字符作为装饰。例如,商品名称“Paradise Towel Wear Co. Beach Coverup << Size Kids XXS >>”不符合要求,因为尺码周围存在过多使用符号的情况。 
+  - 商品名称必须包含可用于清晰描述商品的最少信息,如“Amazon Essentials Dress”“Columbia Hiking Boots”或“Sony Headphones”。 
+  - 商品名称中不能有重复字词。例如,“Baby Boy Outfits Baby Boy fall Winter Clothes Baby Boy Long Sleeve Suspender Outfit Sets”是不符合要求的商品名称。
+- 标题中需要选择1-3 个关键词/产品定位词/消费者用来搜索产品的词语+ 材质/功能/场景等和其他同类产品形成差异化的词语。做到简洁清晰、格式分明、无词语堆砌。
+- 标题参考顺序:品牌名称+大流量的核心关键词商品名称+关键属性(即商品的唯一销售主张)+1-3 个特性词/功能词/属性词+口味/款式+颜色+尺寸+包装数量+型号。例子:
+  - (Brand name) Brushed Grey Towel Bar, 22 Inch 304 Stainless Steel Towel Rack Bathroom, Towel Holder Brushed Grey Wall Mount, Total Length 24 Inch
+  - 根据标题公式,建议颜色/规格信息置后,将核心关键词和特性词提前,尽量避免重复信息。
+  - (Brand Name) Towel Racks for Bathroom,304 Stainless Steel Towel Bar,Wall Mount Towel Holder with Total Length 24 Inch(22 Inch, Brushed Grey)
+  - 核心关键词往前放,3-5 个最佳消费者关注的核心卖点。例如“纯白高纤维棉花脚趾保护套”这个产品,必须把“脚趾保护套”关键词放在标题最前面(这是核心商品关键词),颜色、规格、材质等信息放在后面。
+- 一定要写遵守亚马逊标题格式要求,控制字符,尽量精简善用标点和括号无语法、拼写错误,无中式表达
+- 生成的内容“产品标题、ST搜索词”各个关键词之间只能空格隔开,卖点需要用 '【】' 将重要的卖点括起来。
+
+②关键词权重分配
+- 前60字符:放置核心关键词,确保移动端优先展示
+- 中间部分:补充高转化词和场景词末尾:加入技术参数或独特卖点
+
+观点补充:
+- 根据你生成的 日语的产品标题、ST搜索词、卖点1~5 进行观点补充,解释你为什么要这样选择
+---
+生成内容的格式 {output_type}
+        '''
+        prompt_tmpl = PromptTemplate(template=context_template + prompt_product, )
+        return prompt_tmpl.partial_format(
+            **prompt_keywords,
+            )
+
+    async def get_promp_template_result(self, product_name, prompt: str='',llm_model='', output_type='markdown', verbose=False):
+        llm_name = llm_model or self.llm._get_model_name()
+        models = await AgentContent.find_one(
+            AgentContent.product_name == product_name,
+            AgentContent.model_name==llm_name,
+            )
+        if not models:
+            msg = f"no content for {product_name}"
+            # Either raise a proper exception
+            raise ValueError(msg)
+        main_keys_info = models.competitor.get('markdown')
+        if not main_keys_info:
+            # logger.error(f"AgentContent.competitor.get('markdown') not exist: {product_name}")
+            msg = f"AgentContent.competitor.get('markdown') not exist: {product_name}"
+            raise msg
+        variables = {'product_name': product_name}
+        product_info = await self.template_manager.execute_template("product_info", variables)
+
+        prompt_tmpl = await self.get_promp(
+            product_name, prompt, 
+            prompt_keywords={
+                'llm_keyword_markdown':main_keys_info, 'output_type':output_type, 'product_info':product_info
+                })
+        result = prompt_tmpl.format()
+        logger.info(f"{result}")
+        return result
+    async def acomplete(self, product_name, prompt: str='',llm_model='', output_type='markdown', verbose=False):
+        promp_template_result = await self.get_promp_template_result(product_name, prompt,llm_model, output_type, verbose)
+        response = await self.llm.acomplete(promp_template_result, verbose=verbose)
+        res = self.filter_markdown_content(response.text)
+        logger.info(f"{res}")
+        llm_name = llm_model or self.llm._get_model_name()
+        models = await AgentContent.find_one(
+            AgentContent.product_name == product_name,
+            AgentContent.model_name==llm_name,
+            )
+        if not models.marketing:
+            models.marketing = {}
+        models.marketing[output_type] = res
+        return await models.save()
+
+    async def gen_marketing_file(self, product_name, output_path: str, llm_model: str=''):
+        models = await AgentContent.find(
+            AgentContent.product_name == product_name,
+            ).to_list()
+        content = f"# {product_name}\n\n"
+        for model in models:
+            logger.info(f"{model.product_name} {model.model_name}")
+            marketing = model.marketing.get('markdown')
+            content += f"## {model.model_name}\n\n{marketing}\n\n"
+        promp_template_result = await self.get_promp_template_result(product_name, llm_model=models[0].model_name)
+        content += f"## 提示词\n\n{promp_template_result}\n\n"
+        return save_to_file(content, output_path)
+    
+async def llm_task(product_name='狗刷牙指套'):
+    m = TemplateManager(MONGO_URL, MONGO_DB_NAME)
+    await m.initialize()
+    model = 'openai/groq/llama-3.1-8b-instant'
+    # model = 'groq/groq/qwen-2.5-coder-32b'
+    # model = 'openai/glm-4-flash'
+    # model = 'openai/deepseek-v3'
+    # model = 'openai/groq/qwen-2.5-32b'
+    # model = 'openai/deepseek-chat'
+    model = 'openai/deepseek-reasoner'
+    # model = 'openai/doubao-pro-32k-241215'
+    llm_list = [
+        LiteLLM(model='openai/doubao-pro-32k-241215', api_key=OPENAI_API_KEY, api_base=OPENAI_API_BASE),
+        LiteLLM(model='openai/deepseek-reasoner', api_key=OPENAI_API_KEY, api_base=OPENAI_API_BASE),
+        LiteLLM(model='openai/Qwen/Qwen3-235B-A22B', api_key=OPENAI_API_KEY, api_base=OPENAI_API_BASE),
+        # LiteLLM(model='openai/deepseek-v3', api_key=OPENAI_API_KEY, api_base=OPENAI_API_BASE),
+        # 'openai/QwQ-32B',
+    ]
+    agent_task_list = []
+    for llm in llm_list:
+        model = await AgentContent.find_one(
+            AgentContent.product_name == product_name,
+            AgentContent.model_name==llm.model,
+            )
+        if model:
+            agent = ProductTitleAgent(llm=llm, template_manager=m)
+            agent_task_list.append(agent.acomplete(product_name=product_name))
+    res = await asyncio.gather(*agent_task_list)
+    return
+    task_list = []
+    for llm in llm_list:
+        agent = ProductTitleAgent(llm=llm, template_manager=m)
+        agent_model = agent.get_promp(product_name=product_name)
+        task_list.append(agent_model)
+        # logger.info(f"{agent_model.competitor.items()}")
+    res = await asyncio.gather(*task_list)
+
+async def gen_marketing_file(product_name):
+    m = TemplateManager(MONGO_URL, MONGO_DB_NAME)
+    await m.initialize()
+    llm = LiteLLM(model='openai/doubao-pro-32k-241215', api_key=OPENAI_API_KEY, api_base=OPENAI_API_BASE),
+    agent = ProductTitleAgent(llm=llm, template_manager=m)
+    output_path = r'G:\code\amazone\copywriting_production\output\temp' + f"\\{product_name}-商品标题.md"
+    res = await agent.gen_marketing_file(product_name, output_path)
+    logger.info(f"{res}")
+    return
+
+async def multi_llm_task():
+    list_product_name = [
+        '珍珠纹卸妆棉片',
+        '镊子铲两件套',
+        '折叠剪刀',
+        '脸用刮毛刀4件套',
+        '路亚钓鱼小剪刀',
+    ]
+    llm_task_list = []
+    gen_marketing_file_task_list = []
+    for product_name in list_product_name:
+        llm_task_list.append(llm_task(product_name))
+        gen_marketing_file_task_list.append(gen_marketing_file(product_name))
+    res = await asyncio.gather(*llm_task_list)
+    res = await asyncio.gather(*gen_marketing_file_task_list)
+    return res
+    
+def main():
+    # asyncio.run(llm_task())
+    asyncio.run(multi_llm_task())
+
+if __name__ == "__main__":
+    main()

+ 12 - 3
src/excel_tools/file_manager.py

@@ -69,12 +69,14 @@ class ExcelFileManager:
     def load_s3_extract_data(self) -> list[AsinExtraResultModel]:
         return self.s3_storage_manager.load_s3_complete_extract_data()
 
-async def main():
-    product_name = '园艺镊子套装2件套'
+async def gen_excel_from_template(product_name, template_excel_file):
+    # product_name = '折叠剪刀'
     # product_name = '养花专用园艺迷你3件套'
     self = ExcelFileManager(
         r"G:\code\amazone\copywriting_production\output\resource\extra-data-"+ f"{product_name}.xlsx",
-        r"G:\code\amazone\copywriting_production\output\resource\镊子套装文案制作模版.xlsx")
+        # r"G:\weixin\WeChat Files\wxid_1fmirgx3vudo21\FileStorage\File\2025-05\折叠户外剪刀.xlsx"
+        template_excel_file
+        )
     self.TEMPLATE_PATH
     db_mongo = BaseMongoManager()  # 自动初始化
     await db_mongo.initialize()
@@ -108,7 +110,14 @@ async def main():
     generator.apply_formatting()
     generator.save()
 
+async def main():
+    products_name = [
+        # ['脸用刮毛刀4件套', r"G:\weixin\WeChat Files\wxid_1fmirgx3vudo21\FileStorage\File\2025-05\脸用刮毛刀.xlsx"],
+        ['路亚钓鱼小剪刀', r"G:\weixin\WeChat Files\wxid_1fmirgx3vudo21\FileStorage\File\2025-05\迷你路亚钓鱼剪刀.xlsx"]
 
+    ]
+    for product_name, template_excel_file in products_name:
+        await gen_excel_from_template(product_name, template_excel_file)
 
 if __name__ == "__main__":
     asyncio.run(main())

+ 17 - 20
src/manager/manager_task.py

@@ -29,7 +29,7 @@ class ManagerTask:
 
     def submit_asinseed_task_and_wait(self, asin: str, asin_area: str = 'JP',overwrite:bool=False, timeout: int = 300):
         model = self.db.get_asin_seed(asin)
-        if model and model.mhtml_path:
+        if not overwrite and model and model.mhtml_path:
             logger.info(f"{asin}已经爬取过,跳过")
             return model
         model = AsinSeed(asin=asin, asin_area=asin_area)
@@ -48,11 +48,11 @@ class ManagerTask:
             self.db.save_asin_seed(model)
         return None
     
-    def submit_extract_task_and_wait(self, asin: str, asin_area: str = 'JP', timeout: int = 300):
+    def submit_extract_task_and_wait(self, asin: str, asin_area: str = 'JP', overwrite:bool=False, timeout: int = 300):
         """提交页面解析任务并等待完成,保存结果到数据库"""
         # 从数据库获取mhtml路径
         asin_seed = self.db.get_asin_seed(asin)
-        if asin_seed and asin_seed.extra_result_path:
+        if not overwrite and asin_seed and asin_seed.extra_result_path:
             logger.info(f"{asin}已经解析过,跳过")
             return asin_seed
         if not asin_seed or not asin_seed.mhtml_path:
@@ -311,13 +311,13 @@ async def main():
     # await manager.async_analyze_and_save(product, dry_run=False, over_write=True)
     # await manager.submit_search_mainkeyword(product)
     return
-async def run_asinseed_task(manager:ManagerTask, asinseed_list, over_write=False):
+async def run_asinseed_task(manager:ManagerTask, asinseed_list, overwrite=False):
     # manager = ManagerTask()
     # asinseed_list = ['B09KRH3J5P', 'B0020FO356', 'B07Q194HG1', 'B07PVX118J', ]
     for asin in asinseed_list:
         logger.info(f"{asin}")
-        manager.submit_asinseed_task_and_wait(asin, overwrite=over_write)
-        manager.submit_extract_task_and_wait(asin)
+        manager.submit_asinseed_task_and_wait(asin, overwrite=overwrite)
+        manager.submit_extract_task_and_wait(asin, overwrite=overwrite)
     # result = {'status': 'success', 'path': 's3://public/amazone/copywriting_production/output/B0B658JC22/B0B658JC22.mhtml'}
     # manager.save_task_asin_crawl_result('B0B658JC22', 'JP', result)
 
@@ -325,7 +325,7 @@ async def run_asinseed_task(manager:ManagerTask, asinseed_list, over_write=False
 async def gardening_tools_to_mongo():
     manager = ManagerTask()
     await manager.db_mongo.initialize()
-    product_name = "脚趾保护套"
+    product_name = "路亚钓鱼小剪刀"
     exist_product = await Product.find_one(Product.basic_info.name == product_name)
     if exist_product:
         logger.info(f"产品 {product_name} 已存在")
@@ -333,23 +333,20 @@ async def gardening_tools_to_mongo():
     else:
         product = Product(basic_info=ProductBaseInfo(
             name=product_name,
-            content='20枚',
-            material='银纤维',
-            color='灰色',
-            size='4.5*2cm',
-            package_size='13*9*1.5cm',
-            weight='15g',
-            main_usage='保护脚趾',
+            content='伸缩口1个,钓鱼小剪刀1个',
+            material='不锈钢+ABS手柄',
+            color='红色,绿色',
+            main_usage='用于钓鱼剪PE线,日常携带使用',
             selling_point = [
-                '1.抗菌抑菌',
-                '2.抗臭',
-                '3.吸湿透气',
-                '4.柔软舒适 耐久性好',
+                '1.优质不锈钢,硬度高,不易生锈,更耐用',
+                '2.锯齿刀口,剪切风力,剪PE线咬合更紧,轻松间鱼线',
+                '3.ABS硅胶防滑指垫,操作不打滑。剪尖磨圆处理,防止尖头刺伤手和鱼。小巧迷你,携带方便',
+                '4.内置弹簧,自动回弹,操作更省力。侧滑开关,可闭合。带钥匙扣换,搭配伸缩扣,携带收纳方便',
             ]
         ))
     
-    asinseed_list = ['B0CP94YMWM', 'B0BPN5F3Q6', 'B0CYB1DJM7', 'B0CYBRD55M', 'B09L7W8M3X', 'B0CB5RJCYV']
-    # asinseed_list = ['B0CVX4V2NK', 'B086LDLCW9']
+    asinseed_list = ['B08J3QD3R6', 'B0CBD8NMDH', 'B079ZL8MNG', 'B01MY1FO5Y', 'B0DF86V8XK', 'B0DFYGJV4V', 'B01MY1FO5Y', 'B0C9LB83JG']
+    # asinseed_list = ['B07DKX2KRW', 'B083BRJ7KL', 'B0173688DY', 'B0BX323VM2', 'B092FGVN1D', 'B084VJFX9M']
 
     await run_asinseed_task(manager, asinseed_list)
     for asin in asinseed_list:

+ 2 - 2
src/models/ai_execution_record.py

@@ -114,8 +114,8 @@ class AgentContent(Document):
         description="商品名称" 
     )
     model_name: str = ""
-    marketing: Optional[MarketingInfo] = Field(
-        default=None,
+    marketing: Optional[Dict[str,Union[MarketingInfo,str]]] = Field(
+        default={},
         description="生成的营销内容结果"
     )
     competitor:Optional[Dict[str,Union[AICompetitorAnalyzeMainKeywordsResult,str]]] = Field(

+ 9 - 1
src/readme.md

@@ -12,8 +12,16 @@ $env:CONFIG_PATH="G:\code\amazone\copywriting_production\config\dp_conf\9322_wor
 youka570023@gmail.com U** gg: 7m
 $env:CONFIG_PATH="G:\code\amazone\copywriting_production\config\dp_conf\9323_worker.yaml";celery -A src.tasks.crawl_asin_save_task worker --loglevel=info --hostname=9323@%h
 
+j4732030@gmail.com U*
+$env:CONFIG_PATH="G:\code\amazone\copywriting_production\config\dp_conf\9324_worker.yaml";celery -A src.tasks.crawl_asin_save_task worker --loglevel=info --hostname=9324@%h -Q queue_9324
 
-celery -A config.celery flower 
+mahui8875@gmail.com U*
+$env:CONFIG_PATH="G:\code\amazone\copywriting_production\config\dp_conf\9325_worker.yaml";celery -A src.tasks.crawl_asin_save_task worker --loglevel=info --hostname=9325@%h -Q queue_9325
+
+$env:FLOWER_UNAUTHENTICATED_API="true";celery --broker=redis://sv-v2.lan:7777/0 flower
+# celery -A config.celery flower 
+
+G:\code\amazone\crawler\.venv\Scripts\prefect.exe server start
 
 python -m src.manager.cli_tasks --help
 python -m src.manager.cli_tasks purge-queue --force

+ 3 - 4
utils/file.py

@@ -128,10 +128,9 @@ def save_to_file(content, filename:Path, **extra_args):
     return filename
 
 def read_file(file_uri:str, mode='r'):
-    # if str(file_uri).startswith('s3://'):
-    #     bucket_name, object_name, upload_args = get_s3_uri_info(file_uri)
-    #     response = s3.get_object(Bucket=bucket_name, Key=object_name)
-    #     return response['Body'].read()
+    if not str(file_uri).startswith('http'):
+        with open(file_uri, mode) as f:
+            return f.read()
     with open(file_uri, mode or 'r', transport_params={'client': s3}) as f:
         # 文件存在,继续操作
         return f.read()