Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,13 @@ jobs:
run: uv python install ${{ env.PYTHON_VERSION }}

- name: Install dependencies
run: uv sync --frozen --no-dev

- name: Install dev dependencies
run: uv sync --frozen
run: uv sync --frozen --extra dev

- name: Run ruff check
run: uv run ruff check src/ tests/
run: uv run --frozen --extra dev ruff check src/ tests/

- name: Run ruff format check
run: uv run ruff format --check src/ tests/
run: uv run --frozen --extra dev ruff format --check src/ tests/

# ===== 2. Test(单元测试 + 集成测试)=====
test:
Expand Down Expand Up @@ -84,19 +81,19 @@ jobs:
run: uv python install ${{ env.PYTHON_VERSION }}

- name: Install dependencies
run: uv sync --frozen
run: uv sync --frozen --extra dev

- name: Generate demo data
run: |
# 创建 .env 用于 generate_data.py
echo "OPENAI_API_KEY=ci-test-key" > .env
echo "OPENAI_BASE_URL=https://api.siliconflow.cn/v1" >> .env
uv run python generate_data.py
uv run --frozen --extra dev python generate_data.py
continue-on-error: true

- name: Run tests
run: |
uv run pytest tests/ \
uv run --frozen --extra dev pytest tests/ \
-v \
--tb=short \
--timeout=60 \
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ EXPOSE 8000

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD curl -f http://localhost:8000/api/health || exit 1
CMD [".venv/bin/python", "-c", "from urllib.request import urlopen; urlopen('http://localhost:8000/api/health', timeout=5)"]

# 启动命令
CMD [".venv/bin/uv", "run", "uvicorn", "src.api.main:app", \
CMD [".venv/bin/python", "-m", "uvicorn", "src.api.main:app", \
"--host", "0.0.0.0", "--port", "8000", "--log-level", "info"]
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ dev = [
"pytest>=9.1.0",
"pytest-asyncio>=0.24.0",
"pytest-cov>=6.0.0",
"pytest-timeout>=2.3.1",
"httpx>=0.27.0",
"sentence-transformers>=3.0",
"mcp>=1.27.2",
Expand Down
11 changes: 6 additions & 5 deletions src/agents/comment_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,21 @@
2. 解析 YAML 格式的评论分析数据
3. 输出结构化的分析结果
"""

import re
import yaml
from typing import List, Optional
from pathlib import Path

import yaml
from langchain_core.documents import Document


class CommentAnalyzer:
"""分析笔记评论区,提取用户真实需求信号"""

def __init__(self, raw_dir: Optional[str] = None):
def __init__(self, raw_dir: str | None = None):
self.raw_dir = raw_dir

def analyze(self, documents: List[Document]) -> List[dict]:
def analyze(self, documents: list[Document]) -> list[dict]:
"""
从检索到的文档中提取评论分析数据。

Expand Down Expand Up @@ -98,7 +99,7 @@ def analyze(self, documents: List[Document]) -> List[dict]:

# ---- 内部方法 ----

def _resolve_source(self, doc: Document) -> Optional[Path]:
def _resolve_source(self, doc: Document) -> Path | None:
"""从 Document metadata 中解析原始文件路径"""
source = doc.metadata.get("source", "")
if not source:
Expand Down
75 changes: 47 additions & 28 deletions src/agents/creator_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
与 InsightGenerator 并行:同一份 DemandAggregator 输出,不同的 prompt 模板。
把用户评论数据变成选题 + 脚本大纲 + 封面方案。
"""
from langchain_core.messages import HumanMessage
from langchain_core.prompts import ChatPromptTemplate

from langchain_openai import ChatOpenAI

from src.config import LLM_CONFIG
from src.core.prompt_loader import get_prompt_loader

Expand All @@ -22,19 +22,25 @@ def _get_prompt(self):
return self.prompt_loader.load("creator_report", "v1")

def _build_msg(self, aggregated: dict, category: str = "") -> list:
complaints_str = "\n".join(
f" {i+1}. 「{c}」出现 {f} 次"
for i, (c, f) in enumerate(aggregated["top_complaints"][:10])
) or " 暂无"
complaints_str = (
"\n".join(
f" {i + 1}. 「{c}」出现 {f} 次"
for i, (c, f) in enumerate(aggregated["top_complaints"][:10])
)
or " 暂无"
)

intents_str = "\n".join(
f" {i+1}. 「{t}」出现 {f} 次"
for i, (t, f) in enumerate(aggregated["top_purchase_intents"][:10])
) or " 暂无"
intents_str = (
"\n".join(
f" {i + 1}. 「{t}」出现 {f} 次"
for i, (t, f) in enumerate(aggregated["top_purchase_intents"][:10])
)
or " 暂无"
)

comparisons_str = "\n".join(
f" - {c}" for c in aggregated["comparison_patterns"][:10]
) or " 暂无"
comparisons_str = (
"\n".join(f" - {c}" for c in aggregated["comparison_patterns"][:10]) or " 暂无"
)

brands_str = ", ".join(aggregated["related_brands"]) or "暂无"
differentiations_str = ", ".join(aggregated.get("differentiation_directions", [])) or "暂无"
Expand Down Expand Up @@ -88,19 +94,22 @@ def generate_fallback(self, aggregated: dict, category: str = "") -> str:
lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
lines.append("")
lines.append("【数据亮点】")
lines.append(f" 📊 {aggregated['note_count']}篇笔记 → {len(aggregated['top_complaints'])}个痛点 + {len(aggregated['top_purchase_intents'])}个需求信号")
lines.append(
f" 📊 {aggregated['note_count']}篇笔记 → "
f"{len(aggregated['top_complaints'])}个痛点 + "
f"{len(aggregated['top_purchase_intents'])}个需求信号"
)
lines.append("")

# 提取数据
pains = aggregated["top_complaints"]
intents = aggregated["top_purchase_intents"]
brands = aggregated.get("related_brands", [])
avg_price = aggregated.get("avg_price", 0)
avg_cost = aggregated.get("avg_cost", 0)

# 方案1: 避坑/测评向
lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
lines.append(f"【方案一】🔍 避坑测评")
lines.append("【方案一】🔍 避坑测评")
lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
if pains:
pain = pains[0][0]
Expand All @@ -110,7 +119,10 @@ def generate_fallback(self, aggregated: dict, category: str = "") -> str:
lines.append(f" 标题:《{category}选购避坑指南,买前必看》")
lines.append("")
lines.append(" 【脚本大纲】")
lines.append(f" 前5秒:展示热门{category}产品,抛出问题「{pains[0][0] if pains else '买错等于浪费钱'}」")
lines.append(
f" 前5秒:展示热门{category}产品,"
f"抛出问题「{pains[0][0] if pains else '买错等于浪费钱'}」"
)
lines.append(" 5-15秒:引用真实评论引发共鸣")
if len(pains) >= 2:
lines.append(f" 「{pains[0][0]}」「{pains[1][0]}」")
Expand All @@ -121,7 +133,7 @@ def generate_fallback(self, aggregated: dict, category: str = "") -> str:

# 方案2: 推荐/种草向
lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
lines.append(f"【方案二】🌟 好物种草")
lines.append("【方案二】🌟 好物种草")
lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
if intents:
intent = intents[0][0]
Expand All @@ -133,18 +145,23 @@ def generate_fallback(self, aggregated: dict, category: str = "") -> str:
lines.append(" 【脚本大纲】")
lines.append(f" 前5秒:直接展示{category}使用效果「这也太好用了吧」")
if intents:
lines.append(f" 5-15秒:抛出用户最大需求「{intents[0][0] if intents else '想要高性价比'}」")
lines.append(f" 核心段:第1件→第2件→第3件,每件15秒展示+口播")
lines.append(
f" 5-15秒:抛出用户最大需求「{intents[0][0] if intents else '想要高性价比'}」"
)
lines.append(" 核心段:第1件→第2件→第3件,每件15秒展示+口播")
lines.append(" 结尾:「评论区告诉我你最想试哪款」")
lines.append(" 互动引导:收藏+关注,下期继续挖宝")
lines.append("")

# 方案3: 对比/横评向
lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
lines.append(f"【方案三】⚔️ 品牌横评")
lines.append("【方案三】⚔️ 品牌横评")
lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
if len(brands) >= 2:
lines.append(f" 标题:《{brands[0]} vs {brands[1]} vs {brands[2] if len(brands)>=3 else brands[1]},{category}谁更强?》")
lines.append(
f" 标题:《{brands[0]} vs {brands[1]} vs "
f"{brands[2] if len(brands) >= 3 else brands[1]},{category}谁更强?》"
)
else:
lines.append(f" 标题:《{category}热门品牌横评,到底选哪个?》")
lines.append(" 类型:对比评测 | 平台:B站+小红书 | 预计互动:⭐⭐⭐⭐")
Expand All @@ -159,15 +176,17 @@ def generate_fallback(self, aggregated: dict, category: str = "") -> str:

# 发布建议
lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
lines.append(f"【发布策略】(适用于3个方案)")
lines.append("【发布策略】(适用于3个方案)")
lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
lines.append(" 🕐 推荐发布:工作日 19:00-21:00 或周末 10:00-12:00")
lines.append(f" 🏷️ 核心标签:")
lines.append(f" 方案一:#避坑 #真实测评 #购物踩雷")
lines.append(f" 方案二:#好物推荐 #种草 #年度爱用")
lines.append(f" 方案三:#对比评测 #理性消费 #品牌测评")
lines.append(" 🏷️ 核心标签:")
lines.append(" 方案一:#避坑 #真实测评 #购物踩雷")
lines.append(" 方案二:#好物推荐 #种草 #年度爱用")
lines.append(" 方案三:#对比评测 #理性消费 #品牌测评")
if avg_price > 0:
lines.append(f" 💰 客单价 ¥{avg_price},适合新品牌用方案一打口碑,老品牌用方案二冲销量")
lines.append(
f" 💰 客单价 ¥{avg_price},适合新品牌用方案一打口碑,老品牌用方案二冲销量"
)
lines.append("")

return "\n".join(lines)
58 changes: 32 additions & 26 deletions src/agents/demand_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
将 CommentAnalyzer 输出的多条评论分析结果进行聚合统计,
找出高频投诉、热门需求、品牌竞争格局等可行动的信号。
"""

from collections import Counter
from typing import List, Dict, Any


class DemandAggregator:
"""聚合多个文档的评论数据,提取高价值需求信号"""

def aggregate(self, analyses: List[dict]) -> dict:
def aggregate(self, analyses: list[dict]) -> dict:
"""
输入:CommentAnalyzer 输出的分析结果列表
输出:聚合后的需求洞察 + 电商选品评分
Expand Down Expand Up @@ -56,7 +56,6 @@ def aggregate(self, analyses: List[dict]) -> dict:
total_cost = 0
total_weight = 0.0
profit_margins = []
logistics_scores = []
competition_levels = []
entry_difficulties = []
differentiation_ops = []
Expand Down Expand Up @@ -119,37 +118,46 @@ def aggregate(self, analyses: List[dict]) -> dict:
price_cost_ratio = avg_price / avg_cost
else:
price_cost_ratio = 3.0
profit_score = min(100, int(
min(price_cost_ratio / 5, 1.0) * 40 + # 定价倍率 满分40
min(avg_margin / 0.7, 1.0) * 40 + # 利润率 满分40
20 # 基础分
))
profit_score = min(
100,
int(
min(price_cost_ratio / 5, 1.0) * 40 # 定价倍率 满分40
+ min(avg_margin / 0.7, 1.0) * 40 # 利润率 满分40
+ 20 # 基础分
),
)

# 2️⃣ 物流友好度评分
logistics_score = min(100, int(
(avg_weight < 0.3) * 30 + # 轻量
(avg_weight < 1.0) * 15 + # 不太重
35 + # 基础分
(competition_levels.count("高") < n * 0.5) * 10 # 偏低竞争加分
))
logistics_score = min(
100,
int(
(avg_weight < 0.3) * 30 # 轻量
+ (avg_weight < 1.0) * 15 # 不太重
+ 35 # 基础分
+ (competition_levels.count("高") < n * 0.5) * 10 # 偏低竞争加分
),
)

# 3️⃣ 竞争强度评分(分越高越推荐进入)
low_comp_ratio = competition_levels.count("低") / max(n, 1)
easy_entry_ratio = entry_difficulties.count("低") / max(n, 1)
brand_count = len(all_brands)
competition_score = min(100, int(
low_comp_ratio * 40 + # 低竞争占比
easy_entry_ratio * 30 + # 低进入门槛
min((5 - brand_count) * 5, 20) + # 品牌少=机会大
10 # 基础分
))
competition_score = min(
100,
int(
low_comp_ratio * 40 # 低竞争占比
+ easy_entry_ratio * 30 # 低进入门槛
+ min((5 - brand_count) * 5, 20) # 品牌少=机会大
+ 10 # 基础分
),
)

# 4️⃣ 综合选品评分 (加权)
selection_score = int(
profit_score * 0.30 + # 利润权重 30%
logistics_score * 0.20 + # 物流权重 20%
competition_score * 0.20 + # 竞争权重 20%
demand_score * 0.30 # 需求权重 30%
profit_score * 0.30 # 利润权重 30%
+ logistics_score * 0.20 # 物流权重 20%
+ competition_score * 0.20 # 竞争权重 20%
+ demand_score * 0.30 # 需求权重 30%
)

# 5️⃣ 差异化方向(去重取前5)
Expand All @@ -167,7 +175,6 @@ def aggregate(self, analyses: List[dict]) -> dict:
"total_ask_link": total_ask,
"avg_likes": round(avg_likes, 1),
"demand_score": demand_score,

# 电商选品评分
"avg_price": round(avg_price, 1),
"avg_cost": round(avg_cost, 1),
Expand All @@ -194,7 +201,6 @@ def _empty_result(self) -> dict:
"total_ask_link": 0,
"avg_likes": 0,
"demand_score": 0,

"avg_price": 0,
"avg_cost": 0,
"avg_profit_margin": 0,
Expand Down
Loading
Loading