agentcrew-mcn 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentcrew_mcn-0.1.0.dist-info/METADATA +160 -0
- agentcrew_mcn-0.1.0.dist-info/RECORD +41 -0
- agentcrew_mcn-0.1.0.dist-info/WHEEL +5 -0
- agentcrew_mcn-0.1.0.dist-info/entry_points.txt +2 -0
- agentcrew_mcn-0.1.0.dist-info/licenses/LICENSE +19 -0
- agentcrew_mcn-0.1.0.dist-info/top_level.txt +7 -0
- agents/__init__.py +5 -0
- agents/analyst.py +370 -0
- agents/base.py +115 -0
- agents/publisher.py +262 -0
- agents/skills.py +213 -0
- agents/tools.py +193 -0
- agents/writer.py +285 -0
- cli/__init__.py +3 -0
- cli/analyst.py +210 -0
- cli/main.py +192 -0
- cli/publish.py +193 -0
- cli/rag_cmd.py +188 -0
- cli/schedule.py +183 -0
- cli/write.py +149 -0
- dashboard/__init__.py +1 -0
- dashboard/app.py +162 -0
- dashboard/components.py +224 -0
- dashboard/data_loader.py +178 -0
- dashboard/pages/__init__.py +1 -0
- dashboard/pages/analytics.py +118 -0
- dashboard/pages/publishing.py +102 -0
- dashboard/pages/system.py +98 -0
- llm/__init__.py +5 -0
- llm/client.py +95 -0
- orchestrator/__init__.py +3 -0
- orchestrator/manager.py +248 -0
- orchestrator/scheduler.py +185 -0
- platforms/__init__.py +3 -0
- platforms/base.py +121 -0
- platforms/juejin.py +220 -0
- platforms/zhihu.py +249 -0
- rag/__init__.py +3 -0
- rag/embedder.py +39 -0
- rag/knowledge_base.py +161 -0
- rag/retriever.py +55 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentcrew-mcn
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI MCN 自动推广工具 — 你的AI营销团队,24小时在线工作,不领工资。
|
|
5
|
+
Author: super-rick
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/super-rick/agent-crew
|
|
8
|
+
Project-URL: Bug Reports, https://github.com/super-rick/agent-crew/issues
|
|
9
|
+
Project-URL: Source, https://github.com/super-rick/agent-crew
|
|
10
|
+
Keywords: ai,mcn,marketing,agent,automation,content-marketing,multi-agent
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: openai>=1.0.0
|
|
23
|
+
Requires-Dist: httpx>=0.27.0
|
|
24
|
+
Requires-Dist: click>=8.1.0
|
|
25
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
|
+
Requires-Dist: rich>=13.0.0
|
|
27
|
+
Requires-Dist: chromadb>=0.5.0
|
|
28
|
+
Requires-Dist: schedule>=1.2.0
|
|
29
|
+
Requires-Dist: duckduckgo-search>=6.0.0
|
|
30
|
+
Requires-Dist: streamlit>=1.28.0
|
|
31
|
+
Requires-Dist: pandas>=2.0.0
|
|
32
|
+
Requires-Dist: plotly>=5.0.0
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# AgentCrew 🤖
|
|
36
|
+
|
|
37
|
+
<div align="center">
|
|
38
|
+
|
|
39
|
+
**你的 AI 营销团队,24 小时在线工作,不领工资。**
|
|
40
|
+
|
|
41
|
+
[]()
|
|
42
|
+
[]()
|
|
43
|
+
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 什么是 AgentCrew?
|
|
49
|
+
|
|
50
|
+
AgentCrew 是一个开源的多 Agent 内容营销自动化工具。它由一组 AI"员工"组成,自动完成从内容生成到跨平台分发的全流程。
|
|
51
|
+
|
|
52
|
+
- **Writer Agent** — 文案员工:生成技术文章、帖子、Thread
|
|
53
|
+
- **Publisher Agent** — 运营员工:发布到掘金、知乎、X/Twitter
|
|
54
|
+
- **Analyst Agent** — 数据分析员工:追踪效果、优化策略(v2)
|
|
55
|
+
|
|
56
|
+
## 核心特性
|
|
57
|
+
|
|
58
|
+
- 🧠 **多 Agent 架构** — 每个"员工"独立部署,可插拔
|
|
59
|
+
- 🔧 **Skills + Tools 系统** — 原子化操作 + 高级能力编排
|
|
60
|
+
- 📚 **RAG 知识库** — 基于历史内容和竞品情报的增强生成
|
|
61
|
+
- 🎯 **跨平台发布** — 支持掘金、知乎、X/Twitter 等平台
|
|
62
|
+
- ⏰ **智能调度** — 带随机抖动的定时发布,避免平台检测
|
|
63
|
+
- 🔮 **MCP 预留** — 架构预留 MCP 协议接口(v2 路线图)
|
|
64
|
+
- 📊 **Dashboard** — Web 面板查看发布数据和 Agent 状态(v0.3)
|
|
65
|
+
- 🔁 **自推广 Meta 闭环** — 项目自己推广自己
|
|
66
|
+
|
|
67
|
+
## 快速开始
|
|
68
|
+
|
|
69
|
+
### 安装
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# 1. 克隆项目
|
|
73
|
+
git clone https://github.com/super-rick/agent-crew.git
|
|
74
|
+
cd agent-crew
|
|
75
|
+
|
|
76
|
+
# 2. 安装依赖
|
|
77
|
+
pip install -r requirements.txt
|
|
78
|
+
|
|
79
|
+
# 3. 配置环境变量
|
|
80
|
+
cp .env.example .env
|
|
81
|
+
# 编辑 .env,填入你的 API Key
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 配置
|
|
85
|
+
|
|
86
|
+
编辑 `config.yaml`,填入各平台的认证信息。
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
# config.yaml 核心配置
|
|
90
|
+
llm:
|
|
91
|
+
api_key: ${DEEPSEEK_API_KEY} # 从 .env 读取
|
|
92
|
+
model: deepseek-chat
|
|
93
|
+
|
|
94
|
+
platforms:
|
|
95
|
+
juejin:
|
|
96
|
+
cookie: ${JUEJIN_COOKIE} # 浏览器登录后导出 Cookie
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 使用
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# 生成一篇技术文章
|
|
103
|
+
agent-crew write generate --topic "Python异步编程" --style technical
|
|
104
|
+
|
|
105
|
+
# 发布到掘金
|
|
106
|
+
agent-crew publish post --file article.md --platform juejin
|
|
107
|
+
|
|
108
|
+
# 启动定时发布(每6小时)
|
|
109
|
+
agent-crew schedule start --topic-file topics.txt --platform juejin --interval 6
|
|
110
|
+
|
|
111
|
+
# 管理 RAG 知识库
|
|
112
|
+
agent-crew rag ingest --file article.md --source "my_blog"
|
|
113
|
+
agent-crew rag search --query "AI Agent 架构"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 架构概览
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
┌─────────────────────────────────────────────────────┐
|
|
120
|
+
│ Orchestrator │
|
|
121
|
+
│ 任务分派 / 调度引擎 / 配置管理 │
|
|
122
|
+
└──┬──────────┬──────────────┬────────────────────────┘
|
|
123
|
+
│ │ │
|
|
124
|
+
┌──▼──────┐ ┌▼────────┐ ┌──▼────────┐
|
|
125
|
+
│ Writer │ │Publisher│ │ Analyst │
|
|
126
|
+
│ Agent │ │Agent │ │ Agent │
|
|
127
|
+
│ │ │ │ │ │
|
|
128
|
+
│ Skills: │ │ Skills: │ │ Skills: │
|
|
129
|
+
│ 追热点 │ │ 定时分发 │ │ 效果分析 │
|
|
130
|
+
│ 技术文章 │ │ 多平台 │ │ 趋势报告 │
|
|
131
|
+
│ Thread │ │ 批量 │ │ │
|
|
132
|
+
│ │ │ │ │ │
|
|
133
|
+
│ Tools: │ │ Tools: │ │ Tools: │
|
|
134
|
+
│ search │ │ twitter │ │ analytics │
|
|
135
|
+
│ rag │ │ juejin │ │ compare │
|
|
136
|
+
│ compose │ │ zhihu │ │ report │
|
|
137
|
+
└─────────┘ └─────────┘ └───────────┘
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## 技术栈
|
|
141
|
+
|
|
142
|
+
| 组件 | 技术 |
|
|
143
|
+
|------|------|
|
|
144
|
+
| 语言 | Python 3.10+ |
|
|
145
|
+
| CLI | Click + Rich |
|
|
146
|
+
| LLM | DeepSeek API (OpenAI 兼容) |
|
|
147
|
+
| 向量库 | ChromaDB |
|
|
148
|
+
| 自动化 | Playwright |
|
|
149
|
+
| Dashboard | Streamlit + Plotly (v0.3) |
|
|
150
|
+
|
|
151
|
+
## 开发路线图
|
|
152
|
+
|
|
153
|
+
- **v0.1** ✅ Writer Agent + Publisher Agent + CLI + 掘金/知乎
|
|
154
|
+
- **v0.2** 📝 RAG 深度集成 + X/Twitter + 内容策略引擎
|
|
155
|
+
- **v0.3** 📊 Streamlit Dashboard + Analyst Agent
|
|
156
|
+
- **v1.0** 🎯 MCP 协议 + Skill Store + 社区插件
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
agentcrew_mcn-0.1.0.dist-info/licenses/LICENSE,sha256=pWiDBWmgHOAHWHO7oWN9wrGIk5uHsR62OiDffMNGDhM,1068
|
|
2
|
+
agents/__init__.py,sha256=dW97M43ZI875UMaVpwFrGleTaIE99hCbmLBbXN7Zdx0,120
|
|
3
|
+
agents/analyst.py,sha256=GksoGdySl0UTZeryrn6u8unjbT2ofNmrZ_fA5WuOmE0,14869
|
|
4
|
+
agents/base.py,sha256=AMDS6etwq_KBX0xuz2_g-hnIw-wVto7F4J58DUAqVns,3695
|
|
5
|
+
agents/publisher.py,sha256=8vCl_mIoVfboJDOAzaApn_Eg1fcxxPaY2IT-hTP3up0,9098
|
|
6
|
+
agents/skills.py,sha256=7W1D2W_ZSHkNK_hc9bfZ8CvMgaVfzd0LbeveYod6pFE,7091
|
|
7
|
+
agents/tools.py,sha256=ZxgyOaIlvS-VXVQnDOBTt0sufjm4KBiPbEm04HzvArA,5970
|
|
8
|
+
agents/writer.py,sha256=L9O_VBOjX1bopnFvY1eQrW5qfDj-TTbkW8HaVYXRYWQ,10750
|
|
9
|
+
cli/__init__.py,sha256=hNiA5CRHyJsQDmBH6nTxPsGi8OVAvpoZBR41AE_iMMI,43
|
|
10
|
+
cli/analyst.py,sha256=Fa74cVLIqFH96LotMjc4dHJ9s0K3Yg843-izu5j_7RY,7292
|
|
11
|
+
cli/main.py,sha256=KVWgVizq0OmXabfzTvIWj6AeMk21vX7W0sXxE2iI2zM,6533
|
|
12
|
+
cli/publish.py,sha256=-IiueIBvp5zteYQ_FbzU5YX1dYNizarBhQ-0iny_WrA,6513
|
|
13
|
+
cli/rag_cmd.py,sha256=5qqngrJ2y2mNjRd5KXJCS_0BrfYrFtvnGbfautNcivQ,6242
|
|
14
|
+
cli/schedule.py,sha256=Cslv2uLL_a6SPDw0UypGd0XOv6iZUxz_zOslb08gp7o,5950
|
|
15
|
+
cli/write.py,sha256=1Ekfgsh3mkL4OjH1aY2TZ9PV_NurRIYtu1qGOdFhrPY,5486
|
|
16
|
+
dashboard/__init__.py,sha256=IhhDlwJtlXQ1btV91XY9UCKQ9W8-lMHjGc20AuEdO0c,60
|
|
17
|
+
dashboard/app.py,sha256=Hkhbr-xWDBoT80MumcWt0CzpsrmAMRM_gQ6TqjKsJqk,5412
|
|
18
|
+
dashboard/components.py,sha256=Kcgp7lhT58R4sbpCXSW3Lo9ZD0gH9BMShmBBHu9eTTU,7199
|
|
19
|
+
dashboard/data_loader.py,sha256=ECB9-eKXBWYtUve8Jdvc8GF3dUlvxUi5Ajbe6MT7bm0,6022
|
|
20
|
+
dashboard/pages/__init__.py,sha256=_Q80Xal7cQFs9RD1Rp7CteTNr4Pxzg4Ij2Onx2Lx5Cs,31
|
|
21
|
+
dashboard/pages/analytics.py,sha256=HXJqeU8KEBwkvgBecyOKhlf5YfZS-qT71oh-U2E9Yeo,4383
|
|
22
|
+
dashboard/pages/publishing.py,sha256=6Zv55nqoP9yJTmgjfXAP1fl0489JlJnmGxGfwV1sK5Q,3169
|
|
23
|
+
dashboard/pages/system.py,sha256=el_MQH2spmqtCVWp21IbSQ-wJdwP9YOVq1rQ0XvvE8k,3263
|
|
24
|
+
llm/__init__.py,sha256=nH01TcT4p0OsXfEcwvTkAJuWDALrn1_fs4NGcFhkg5k,129
|
|
25
|
+
llm/client.py,sha256=a-ltaD0-y_X8cGDsARHSbGE5y6lH1Zri2lAq_JFwI3A,2803
|
|
26
|
+
orchestrator/__init__.py,sha256=SeyxV-nEBS0prfA0V4jVnOykfxoQa21L87PeHG_BNTw,52
|
|
27
|
+
orchestrator/manager.py,sha256=n7l6I397__6h_Z7QUEXBZzWsbgzX2l6BR6WGI2DuhEI,8933
|
|
28
|
+
orchestrator/scheduler.py,sha256=pcNaGBFBZ6tPaP8wVSIdjww6adm4LRx-hr-o2cWZh5M,5902
|
|
29
|
+
platforms/__init__.py,sha256=iF3yrPVPUFgmLnYwyCc-XgtiAXeSJhLBCaCXHP02jd8,57
|
|
30
|
+
platforms/base.py,sha256=-iFQOQ99NHCq_LGSaSUAlj0z9OrcdPMCoXhFygqk_Hc,3578
|
|
31
|
+
platforms/juejin.py,sha256=bdscTIVVKmvudcPfkOdOyZRjrwrn-1Q3Jl25grio8uo,7716
|
|
32
|
+
platforms/zhihu.py,sha256=cDL7JDxNmfWKPpsp3netNxDIKCYXnJB-GWq3GKYTMQc,9431
|
|
33
|
+
rag/__init__.py,sha256=5SWzdS-WPJTmrl-ocHlwg1gsbs2ffboNqZu7HVdQlpA,43
|
|
34
|
+
rag/embedder.py,sha256=9Ybw8PCLzJqsEiuS4v6-Y8HbaOpSLYCfBeaTQCYdT6w,1287
|
|
35
|
+
rag/knowledge_base.py,sha256=_0-OPrzKpckJTnIeM-1I4geKySxArB_IW7oMX3_NKm0,4822
|
|
36
|
+
rag/retriever.py,sha256=QqtK3CIoIPy_5HEYAS-S76v-_CMsQ4oookQMnrwZEm8,2027
|
|
37
|
+
agentcrew_mcn-0.1.0.dist-info/METADATA,sha256=Je7izE0alkipUD_3nzyfvAEKhZ-xAgVoYkdxVzpGcoA,5525
|
|
38
|
+
agentcrew_mcn-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
39
|
+
agentcrew_mcn-0.1.0.dist-info/entry_points.txt,sha256=mtl8Gy_9FaMkpkJMhWUL-RpBXFhNX2rmgMA9KaN9b0I,45
|
|
40
|
+
agentcrew_mcn-0.1.0.dist-info/top_level.txt,sha256=ORi6zM-akZpk2mzc-czgFr7DJj7yvpErn_tscUvzGyY,52
|
|
41
|
+
agentcrew_mcn-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) {{ year }} {{ organization }}
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
14
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
15
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
16
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
17
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
18
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
|
19
|
+
OR OTHER DEALINGS IN THE SOFTWARE.
|
agents/__init__.py
ADDED
agents/analyst.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
"""
|
|
3
|
+
Analyst Agent — AI 数据分析员工。
|
|
4
|
+
|
|
5
|
+
负责:
|
|
6
|
+
1. 效果分析(analyze)— 聚合发布历史数据,统计各平台表现
|
|
7
|
+
2. 报告生成(report)— 调用 LLM 生成可读的效果报告
|
|
8
|
+
3. 策略建议(recommend)— 基于数据生成内容策略优化建议
|
|
9
|
+
|
|
10
|
+
数据源:直接读取 data/post_history.json(独立于 PublisherAgent)。
|
|
11
|
+
核心流程:
|
|
12
|
+
execute(task) →
|
|
13
|
+
task.params.action: "analyze" | "report" | "recommend"
|
|
14
|
+
→ _load_history() → _filter → _calculate_metrics → (LLM 可选) → TaskResult
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from collections import defaultdict
|
|
19
|
+
from datetime import datetime, timedelta
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from agents.base import BaseAgent, Task, TaskResult
|
|
24
|
+
from llm.client import LLMClient
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AnalystAgent(BaseAgent):
|
|
28
|
+
"""Content performance analysis agent — the AI data analyst employee.
|
|
29
|
+
|
|
30
|
+
Independent of PublisherAgent — reads post history directly from the
|
|
31
|
+
shared history file (data/post_history.json).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
name = "analyst"
|
|
35
|
+
description = "负责内容效果数据分析、报告生成和策略优化建议"
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
llm_client: LLMClient,
|
|
40
|
+
config: dict | None = None,
|
|
41
|
+
):
|
|
42
|
+
super().__init__(llm_client, config)
|
|
43
|
+
self._history_file: str = (
|
|
44
|
+
config.get("history_file", "data/post_history.json")
|
|
45
|
+
if config
|
|
46
|
+
else "data/post_history.json"
|
|
47
|
+
)
|
|
48
|
+
self._default_days: int = (
|
|
49
|
+
config.get("default_days", 7) if config else 7
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def get_system_prompt(self) -> str:
|
|
53
|
+
return (
|
|
54
|
+
"你是一个专业的内容运营数据分析师。"
|
|
55
|
+
"你通过分析各平台发布数据,识别内容表现趋势和优化机会。"
|
|
56
|
+
"你的分析结论应当数据驱动、可执行、有具体行动建议。"
|
|
57
|
+
"你使用中文输出,语言简洁专业。"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# ── Public API ──────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
def execute(self, task: Task) -> TaskResult:
|
|
63
|
+
"""Execute an analyst task.
|
|
64
|
+
|
|
65
|
+
Task params:
|
|
66
|
+
action (str): analyze | report | recommend (default: analyze)
|
|
67
|
+
days (int): 分析最近多少天的数据,默认取 config.default_days (7)
|
|
68
|
+
platforms (list[str], optional): 按平台过滤
|
|
69
|
+
"""
|
|
70
|
+
started_at = datetime.now()
|
|
71
|
+
action = task.params.get("action", "analyze")
|
|
72
|
+
days = task.params.get("days", self._default_days)
|
|
73
|
+
platforms = task.params.get("platforms")
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
# Step 1: Collect and filter data
|
|
77
|
+
history = self._load_history()
|
|
78
|
+
filtered = self._filter_by_days(history, days)
|
|
79
|
+
if platforms:
|
|
80
|
+
filtered = [r for r in filtered if r.get("platform") in platforms]
|
|
81
|
+
|
|
82
|
+
# Step 2: Calculate metrics
|
|
83
|
+
analysis = self._calculate_metrics(filtered, days)
|
|
84
|
+
|
|
85
|
+
if action == "analyze":
|
|
86
|
+
completed_at = datetime.now()
|
|
87
|
+
return TaskResult(
|
|
88
|
+
task_id=task.task_id,
|
|
89
|
+
success=True,
|
|
90
|
+
data={
|
|
91
|
+
"action": "analyze",
|
|
92
|
+
"period_days": days,
|
|
93
|
+
**analysis,
|
|
94
|
+
},
|
|
95
|
+
started_at=started_at,
|
|
96
|
+
completed_at=completed_at,
|
|
97
|
+
duration_seconds=(completed_at - started_at).total_seconds(),
|
|
98
|
+
agent_name=self.name,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
elif action == "report":
|
|
102
|
+
report_text = self._generate_report(analysis)
|
|
103
|
+
completed_at = datetime.now()
|
|
104
|
+
return TaskResult(
|
|
105
|
+
task_id=task.task_id,
|
|
106
|
+
success=True,
|
|
107
|
+
data={
|
|
108
|
+
"action": "report",
|
|
109
|
+
"period_days": days,
|
|
110
|
+
"report": report_text,
|
|
111
|
+
**analysis,
|
|
112
|
+
},
|
|
113
|
+
started_at=started_at,
|
|
114
|
+
completed_at=completed_at,
|
|
115
|
+
duration_seconds=(completed_at - started_at).total_seconds(),
|
|
116
|
+
agent_name=self.name,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
elif action == "recommend":
|
|
120
|
+
recommend_text = self._generate_recommendations(analysis)
|
|
121
|
+
completed_at = datetime.now()
|
|
122
|
+
return TaskResult(
|
|
123
|
+
task_id=task.task_id,
|
|
124
|
+
success=True,
|
|
125
|
+
data={
|
|
126
|
+
"action": "recommend",
|
|
127
|
+
"period_days": days,
|
|
128
|
+
"recommendations": recommend_text,
|
|
129
|
+
**analysis,
|
|
130
|
+
},
|
|
131
|
+
started_at=started_at,
|
|
132
|
+
completed_at=completed_at,
|
|
133
|
+
duration_seconds=(completed_at - started_at).total_seconds(),
|
|
134
|
+
agent_name=self.name,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
else:
|
|
138
|
+
raise ValueError(
|
|
139
|
+
f"Unknown action for analyst: {action!r}. "
|
|
140
|
+
f"Expected: analyze, report, recommend"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
completed_at = datetime.now()
|
|
145
|
+
return TaskResult(
|
|
146
|
+
task_id=task.task_id,
|
|
147
|
+
success=False,
|
|
148
|
+
error_message=str(e),
|
|
149
|
+
started_at=started_at,
|
|
150
|
+
completed_at=completed_at,
|
|
151
|
+
duration_seconds=(completed_at - started_at).total_seconds(),
|
|
152
|
+
agent_name=self.name,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# ── Data Loading ────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
def _load_history(self) -> list[dict]:
|
|
158
|
+
"""Load post history from the shared history file.
|
|
159
|
+
|
|
160
|
+
Independent of PublisherAgent — reads directly from the JSON file
|
|
161
|
+
that PublisherAgent writes to. Returns empty list if the file
|
|
162
|
+
doesn't exist or is malformed (first run, no posts yet).
|
|
163
|
+
"""
|
|
164
|
+
path = Path(self._history_file)
|
|
165
|
+
if not path.exists():
|
|
166
|
+
return []
|
|
167
|
+
try:
|
|
168
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
169
|
+
return json.load(f)
|
|
170
|
+
except (json.JSONDecodeError, OSError):
|
|
171
|
+
return []
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _filter_by_days(records: list[dict], days: int) -> list[dict]:
|
|
175
|
+
"""Keep only records from the last N days."""
|
|
176
|
+
if not records:
|
|
177
|
+
return []
|
|
178
|
+
cutoff = datetime.now() - timedelta(days=days)
|
|
179
|
+
result: list[dict] = []
|
|
180
|
+
for r in records:
|
|
181
|
+
posted_str = r.get("posted_at")
|
|
182
|
+
if not posted_str:
|
|
183
|
+
continue
|
|
184
|
+
try:
|
|
185
|
+
posted_dt = datetime.fromisoformat(posted_str)
|
|
186
|
+
if posted_dt >= cutoff:
|
|
187
|
+
result.append(r)
|
|
188
|
+
except (ValueError, TypeError):
|
|
189
|
+
continue
|
|
190
|
+
return result
|
|
191
|
+
|
|
192
|
+
# ── Metrics Calculation ─────────────────────────────────
|
|
193
|
+
|
|
194
|
+
def _calculate_metrics(self, records: list[dict], days: int) -> dict[str, Any]:
|
|
195
|
+
"""Aggregate post records into structured metrics.
|
|
196
|
+
|
|
197
|
+
Returns dict with:
|
|
198
|
+
total_posts, success_count, fail_count, success_rate
|
|
199
|
+
platform_stats: [{platform, total, success, fail, rate}, ...]
|
|
200
|
+
daily_counts: [{date, total, success, fail}, ...]
|
|
201
|
+
recent_fails: [{platform, error, posted_at}, ...]
|
|
202
|
+
error_summary: {error_type: count, ...}
|
|
203
|
+
"""
|
|
204
|
+
total = len(records)
|
|
205
|
+
success = sum(1 for r in records if r.get("success"))
|
|
206
|
+
failed = total - success
|
|
207
|
+
success_rate = (success / total * 100) if total else 0.0
|
|
208
|
+
|
|
209
|
+
# Per-platform stats
|
|
210
|
+
platform_buckets: dict[str, dict] = defaultdict(
|
|
211
|
+
lambda: {"total": 0, "success": 0, "fail": 0}
|
|
212
|
+
)
|
|
213
|
+
for r in records:
|
|
214
|
+
plat = r.get("platform", "unknown")
|
|
215
|
+
platform_buckets[plat]["total"] += 1
|
|
216
|
+
if r.get("success"):
|
|
217
|
+
platform_buckets[plat]["success"] += 1
|
|
218
|
+
else:
|
|
219
|
+
platform_buckets[plat]["fail"] += 1
|
|
220
|
+
|
|
221
|
+
platform_stats: list[dict] = []
|
|
222
|
+
for plat, pb in sorted(platform_buckets.items()):
|
|
223
|
+
p_rate = (pb["success"] / pb["total"] * 100) if pb["total"] else 0.0
|
|
224
|
+
platform_stats.append({
|
|
225
|
+
"platform": plat,
|
|
226
|
+
"total": pb["total"],
|
|
227
|
+
"success": pb["success"],
|
|
228
|
+
"fail": pb["fail"],
|
|
229
|
+
"success_rate": round(p_rate, 1),
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
# Daily counts (fill gaps with zero)
|
|
233
|
+
now = datetime.now()
|
|
234
|
+
daily_map: dict[str, dict] = defaultdict(
|
|
235
|
+
lambda: {"total": 0, "success": 0, "fail": 0}
|
|
236
|
+
)
|
|
237
|
+
for r in records:
|
|
238
|
+
posted_str = r.get("posted_at")
|
|
239
|
+
if not posted_str:
|
|
240
|
+
continue
|
|
241
|
+
try:
|
|
242
|
+
d = datetime.fromisoformat(posted_str).strftime("%Y-%m-%d")
|
|
243
|
+
daily_map[d]["total"] += 1
|
|
244
|
+
if r.get("success"):
|
|
245
|
+
daily_map[d]["success"] += 1
|
|
246
|
+
else:
|
|
247
|
+
daily_map[d]["fail"] += 1
|
|
248
|
+
except (ValueError, TypeError):
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
daily_counts: list[dict] = []
|
|
252
|
+
for i in range(days - 1, -1, -1):
|
|
253
|
+
day_label = (now - timedelta(days=i)).strftime("%Y-%m-%d")
|
|
254
|
+
d = daily_map.get(day_label, {"total": 0, "success": 0, "fail": 0})
|
|
255
|
+
daily_counts.append({
|
|
256
|
+
"date": day_label,
|
|
257
|
+
"total": d["total"],
|
|
258
|
+
"success": d["success"],
|
|
259
|
+
"fail": d["fail"],
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
# Recent failures (up to 5)
|
|
263
|
+
recent_fails = [
|
|
264
|
+
{
|
|
265
|
+
"platform": r.get("platform"),
|
|
266
|
+
"error": r.get("error_message"),
|
|
267
|
+
"posted_at": r.get("posted_at"),
|
|
268
|
+
}
|
|
269
|
+
for r in records
|
|
270
|
+
if not r.get("success")
|
|
271
|
+
][-5:]
|
|
272
|
+
|
|
273
|
+
# Error type summary
|
|
274
|
+
error_counts: dict[str, int] = defaultdict(int)
|
|
275
|
+
for r in records:
|
|
276
|
+
if not r.get("success") and r.get("error_message"):
|
|
277
|
+
err = r["error_message"]
|
|
278
|
+
key = err.split(":")[0].strip() if ":" in err else err[:80]
|
|
279
|
+
error_counts[key] += 1
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
"total_posts": total,
|
|
283
|
+
"success_count": success,
|
|
284
|
+
"fail_count": failed,
|
|
285
|
+
"success_rate": round(success_rate, 1),
|
|
286
|
+
"platform_stats": platform_stats,
|
|
287
|
+
"daily_counts": daily_counts,
|
|
288
|
+
"recent_fails": recent_fails,
|
|
289
|
+
"error_summary": dict(
|
|
290
|
+
sorted(error_counts.items(), key=lambda x: -x[1])
|
|
291
|
+
),
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
# ── LLM-powered Generation ──────────────────────────────
|
|
295
|
+
|
|
296
|
+
def _generate_report(self, metrics: dict[str, Any]) -> str:
|
|
297
|
+
"""Generate a human-readable performance report via LLM."""
|
|
298
|
+
prompt = self._build_report_prompt(metrics)
|
|
299
|
+
messages = self._build_messages(prompt)
|
|
300
|
+
return self.llm_client.chat(messages)
|
|
301
|
+
|
|
302
|
+
def _generate_recommendations(self, metrics: dict[str, Any]) -> str:
|
|
303
|
+
"""Generate strategy optimisation recommendations via LLM."""
|
|
304
|
+
prompt = self._build_recommend_prompt(metrics)
|
|
305
|
+
messages = self._build_messages(prompt)
|
|
306
|
+
return self.llm_client.chat(messages)
|
|
307
|
+
|
|
308
|
+
# ── Prompt Builders ─────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
def _build_report_prompt(self, metrics: dict) -> str:
|
|
311
|
+
"""Build the LLM prompt for report generation."""
|
|
312
|
+
plat_rows = "\n".join(
|
|
313
|
+
f" - {p['platform']}: 共发布 {p['total']} 篇, "
|
|
314
|
+
f"成功 {p['success']} 篇, 失败 {p['fail']} 篇, "
|
|
315
|
+
f"成功率 {p['success_rate']}%"
|
|
316
|
+
for p in (metrics.get("platform_stats") or [])
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
daily_rows = "\n".join(
|
|
320
|
+
f" - {d['date']}: 发布 {d['total']} 篇 "
|
|
321
|
+
f"(成功 {d['success']}, 失败 {d['fail']})"
|
|
322
|
+
for d in (metrics.get("daily_counts") or [])
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
fail_rows = "\n".join(
|
|
326
|
+
f" - [{f.get('platform', '?')}] {f.get('error', '未知错误')}"
|
|
327
|
+
for f in (metrics.get("recent_fails") or [])
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
"请根据以下发布数据生成一份内容运营周报。\n\n"
|
|
332
|
+
f"## 总览\n"
|
|
333
|
+
f"- 统计周期: 过去 {metrics.get('period_days', '?')} 天\n"
|
|
334
|
+
f"- 发布总数: {metrics['total_posts']} 篇\n"
|
|
335
|
+
f"- 成功: {metrics['success_count']} 篇 ({metrics['success_rate']}%)\n"
|
|
336
|
+
f"- 失败: {metrics['fail_count']} 篇\n\n"
|
|
337
|
+
f"## 各平台表现\n{plat_rows if plat_rows else ' (无数据)'}\n\n"
|
|
338
|
+
f"## 每日发布\n{daily_rows if daily_rows else ' (无数据)'}\n\n"
|
|
339
|
+
f"## 近期失败\n{fail_rows if fail_rows else ' (无失败记录)'}\n\n"
|
|
340
|
+
"请生成周报,包含:\n"
|
|
341
|
+
"1. 核心发现(3-5 条关键结论)\n"
|
|
342
|
+
"2. 各平台表现评价\n"
|
|
343
|
+
"3. 失败原因分析与改进建议\n"
|
|
344
|
+
"4. 下一步行动计划\n\n"
|
|
345
|
+
"格式:Markdown,标题层级分明,数据引用准确。"
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
def _build_recommend_prompt(self, metrics: dict) -> str:
|
|
349
|
+
"""Build the LLM prompt for strategy recommendations."""
|
|
350
|
+
plat_rows = "\n".join(
|
|
351
|
+
f" - {p['platform']}: {p['total']} 篇, "
|
|
352
|
+
f"成功率 {p['success_rate']}%"
|
|
353
|
+
for p in (metrics.get("platform_stats") or [])
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
return (
|
|
357
|
+
"你是一个内容运营策略顾问。请根据以下发布数据给出优化建议。\n\n"
|
|
358
|
+
f"## 当前数据\n"
|
|
359
|
+
f"- 统计周期: 过去 {metrics.get('period_days', '?')} 天\n"
|
|
360
|
+
f"- 发布总数: {metrics['total_posts']} 篇\n"
|
|
361
|
+
f"- 成功率: {metrics['success_rate']}%\n\n"
|
|
362
|
+
f"## 各平台\n{plat_rows if plat_rows else ' (无数据)'}\n\n"
|
|
363
|
+
"请从以下维度给出具体可执行的策略建议:\n"
|
|
364
|
+
"1. 平台策略 — 哪些平台应加大/减少投入\n"
|
|
365
|
+
"2. 发布频率 — 建议的节奏\n"
|
|
366
|
+
"3. 内容方向 — 什么内容类型效果更好\n"
|
|
367
|
+
"4. 发布时机 — 最佳发布时间建议\n"
|
|
368
|
+
"5. 技术改进 — 降低发布失败率的措施\n\n"
|
|
369
|
+
"建议应数据驱动、具体可执行,给出优先级排序。"
|
|
370
|
+
)
|