feat: weekly_digest.py - 周度日报合成分析 (读取7份日报→AI合成周报)
This commit is contained in:
367
analyzers/weekly_digest.py
Normal file
367
analyzers/weekly_digest.py
Normal file
@ -0,0 +1,367 @@
|
||||
"""Weekly digest synthesizer - reads all daily digests and generates a week-level analysis."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from tools.config import load_secrets, get_output_dir
|
||||
from tools.llm import DeepSeekClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TZ_BEIJING = timezone(timedelta(hours=8))
|
||||
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
WEEKLY_SYSTEM_PROMPT = """你是一个私人思考伙伴。你的任务是认真阅读用户过去一周的7份日报,写一篇有深度的周度分析报告。
|
||||
|
||||
## 核心原则
|
||||
|
||||
你读的是日报——不是原始灵感记录,而是已经经过每日分析提炼的内容。你的工作是:
|
||||
|
||||
1. **提炼跨日主线**:7份日报各自都有"今日概览"——把它们串起来。这周的主旋律是什么?从周一到现在,思考的焦点如何演变?
|
||||
|
||||
2. **追踪延续与断裂**:日报中反复出现的标签(如 #浙江培训 #交易 #灵感收集器)代表持续性的主题。哪些主题从头贯穿到尾?哪些在中间消失了?为什么?
|
||||
|
||||
3. **识别本周的关键跃迁**:在7天的思考中,哪一天出现了认知上的质变?哪一个洞察是本周最核心的收获?
|
||||
|
||||
4. **不逐日罗列**:不要"周一...周二...周三..."。要融会贯通,把7天当作一个整体来写。按主题组织,不是按时间组织。
|
||||
|
||||
5. **深度优先**:每周分析应该比每日分析更深一层。如果日报是在追问"今天发生了什么",周报应该回答"这一周意味着什么"。
|
||||
|
||||
## 输出格式
|
||||
|
||||
纯 Markdown,不要代码块包裹。
|
||||
|
||||
结构参考:
|
||||
|
||||
### 1. 本周概览
|
||||
一两句话点出这周的底色,并指出最核心的1-2条暗线(可能不是最显眼的,但是对用户最具启发价值的)。
|
||||
|
||||
### 2. 深入分析
|
||||
按主题组织,融会贯通地写。可以包含但不限于:
|
||||
- 持续性主题的演变轨迹
|
||||
- 认知跃迁的关键时刻
|
||||
- 看似无关事件之间的深层关联
|
||||
- 本周形成的新规则、新认知、新能力
|
||||
- 与用户长期目标/身份认同的关系
|
||||
|
||||
引用日报内容要自然嵌入行文,让用户知道你在引用哪一天的观察。
|
||||
|
||||
### 3. 待办事项
|
||||
基于本周分析,提出需要延续到下周的具体行动。要写到"什么时机做什么做到什么程度"的颗粒度。
|
||||
|
||||
---
|
||||
|
||||
注意:即便7份日报中有些比较简短,也要从整体中挖掘深度。篇幅不设上限。"""
|
||||
|
||||
|
||||
def read_week_digests(week_start, week_end):
|
||||
"""Read all daily digest files from week_start to week_end.
|
||||
|
||||
Returns list of (date_str, digest_text, tags) tuples.
|
||||
"""
|
||||
daily_root = get_output_dir("daily")
|
||||
digests = []
|
||||
|
||||
current = week_start
|
||||
while current <= week_end:
|
||||
date_str = current.strftime("%Y-%m-%d")
|
||||
daily_dir = os.path.join(daily_root, date_str)
|
||||
|
||||
if os.path.isdir(daily_dir):
|
||||
# Find the latest digest file for this day
|
||||
files = sorted([
|
||||
f for f in os.listdir(daily_dir)
|
||||
if f.endswith(".md") and "digest" in f and not f.endswith(".bak.md")
|
||||
], reverse=True)
|
||||
|
||||
if files:
|
||||
filepath = os.path.join(daily_dir, files[0])
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Extract AI analysis section
|
||||
ai_start = content.find("## AI 分析")
|
||||
annotation_start = content.find("## 我的批注", ai_start) if ai_start != -1 else -1
|
||||
|
||||
if ai_start != -1 and annotation_start != -1:
|
||||
ai_body = content[ai_start:annotation_start].strip()
|
||||
elif ai_start != -1:
|
||||
ai_body = content[ai_start:].strip()
|
||||
else:
|
||||
ai_body = content
|
||||
|
||||
# Extract tags from frontmatter
|
||||
tags = []
|
||||
if "tags:" in content[:500]:
|
||||
tag_line = content.split("tags:")[1].split("\n")[0]
|
||||
import re
|
||||
tags = re.findall(r"'([^']+)'", tag_line)
|
||||
|
||||
digests.append((date_str, ai_body, tags))
|
||||
logger.info(
|
||||
"Read digest %s: %d chars, tags=%s",
|
||||
date_str, len(ai_body), ", ".join(tags[:5])
|
||||
)
|
||||
else:
|
||||
logger.info("No digest file found for %s (skipped)", date_str)
|
||||
else:
|
||||
logger.info("No daily dir for %s (skipped)", date_str)
|
||||
|
||||
current += timedelta(days=1)
|
||||
|
||||
return digests
|
||||
|
||||
|
||||
def build_weekly_prompt(digests):
|
||||
"""Build the user prompt containing all 7 daily digest bodies."""
|
||||
|
||||
if not digests:
|
||||
return None
|
||||
|
||||
# Extract week range
|
||||
first_date = digests[0][0]
|
||||
last_date = digests[-1][0]
|
||||
|
||||
prompt_parts = [
|
||||
f"以下是我从 {first_date} 到 {last_date} 共7天的日报。",
|
||||
f"共 {len(digests)} 份日报。",
|
||||
"",
|
||||
"请基于这些日报内容,写一篇周度分析报告。"
|
||||
]
|
||||
|
||||
# Collect all unique tags across the week
|
||||
all_tags = set()
|
||||
for _, _, tags in digests:
|
||||
all_tags.update(tags)
|
||||
if all_tags:
|
||||
tag_str = "、".join(f"#{t}" for t in sorted(all_tags))
|
||||
prompt_parts.append(f"\n本周出现的标签:{tag_str}")
|
||||
|
||||
prompt_parts.append("\n---\n")
|
||||
|
||||
for date_str, body, tags in digests:
|
||||
# Show a weekday indicator
|
||||
dt = datetime.strptime(date_str, "%Y-%m-%d")
|
||||
weekday = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"][dt.weekday()]
|
||||
tag_hint = ""
|
||||
if tags:
|
||||
important_tags = [t for t in tags if t not in ("灵感收集器", "每日总结", "AI分析")]
|
||||
if important_tags:
|
||||
tag_hint = " | 标签:" + ", ".join(f"#{t}" for t in important_tags[:5])
|
||||
|
||||
prompt_parts.append(f"\n## {date_str}({weekday}){tag_hint}\n")
|
||||
prompt_parts.append(body)
|
||||
prompt_parts.append("\n")
|
||||
|
||||
prompt_parts.append("\n---\n")
|
||||
prompt_parts.append("请基于以上所有日报内容,写一篇有深度的周度分析报告。")
|
||||
|
||||
return "\n".join(prompt_parts)
|
||||
|
||||
|
||||
def format_weekly_digest(week_start, week_end, ai_body, tags=None):
|
||||
"""Format weekly digest markdown with frontmatter, matching daily digest format."""
|
||||
week_start_str = week_start.strftime("%Y-%m-%d")
|
||||
week_end_str = week_end.strftime("%m/%d")
|
||||
week_number = week_start.isocalendar()[1]
|
||||
start_month = week_start.strftime("%m/%d")
|
||||
|
||||
char_count = len(ai_body.replace(" ", "").replace("\n", ""))
|
||||
read_min = max(1, round(char_count / 300))
|
||||
|
||||
if tags is None:
|
||||
tags = ["灵感收集器", "每周总结"]
|
||||
tag_str = ", ".join(["'" + t + "'" for t in tags])
|
||||
|
||||
lines = []
|
||||
lines.append("---")
|
||||
lines.append(f"date: {week_start.strftime('%Y')}-W{week_number}")
|
||||
lines.append("type: weekly-digest")
|
||||
lines.append(f"tags: [{tag_str}]")
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
lines.append(f"# 第{week_number}周 · 周度总结({start_month} - {week_end_str})")
|
||||
lines.append("")
|
||||
lines.append(f"> 本文共 **{char_count}** 字 · 预计阅读 **{read_min}** 分钟")
|
||||
lines.append("")
|
||||
lines.append("> 基于本周7份日报综合生成。")
|
||||
lines.append("")
|
||||
lines.append("## AI 分析(自动生成,请勿编辑)")
|
||||
lines.append("")
|
||||
lines.append(ai_body)
|
||||
lines.append("")
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
lines.append("## 我的批注")
|
||||
lines.append("")
|
||||
lines.append("> *在这里写下你的想法、质疑、补充*")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def git_push_weekly(filepath):
|
||||
"""Commit and push the weekly digest to Gitea."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "add", filepath],
|
||||
cwd=PROJECT_DIR,
|
||||
capture_output=True, text=True, timeout=15
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.warning("git add failed: %s", result.stderr.strip())
|
||||
return False
|
||||
|
||||
result = subprocess.run(
|
||||
["git", "status", "--porcelain", "ai-insights/"],
|
||||
cwd=PROJECT_DIR,
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if not result.stdout.strip():
|
||||
logger.info("No changes to commit")
|
||||
return True
|
||||
|
||||
week_num = datetime.now(TZ_BEIJING).isocalendar()[1]
|
||||
result = subprocess.run(
|
||||
["git", "commit", "-m", f"weekly digest W{week_num}"],
|
||||
cwd=PROJECT_DIR,
|
||||
capture_output=True, text=True, timeout=15
|
||||
)
|
||||
if result.returncode != 0 and "nothing to commit" not in result.stdout:
|
||||
logger.warning("git commit failed: %s", result.stderr.strip())
|
||||
return False
|
||||
|
||||
result = subprocess.run(
|
||||
["git", "push", "origin", "main"],
|
||||
cwd=PROJECT_DIR,
|
||||
capture_output=True, text=True, timeout=30
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.warning("git push failed: %s", result.stderr.strip())
|
||||
return False
|
||||
|
||||
for line in result.stdout.split("\n"):
|
||||
if "->" in line or "remote:" in line:
|
||||
logger.info("Gitea push: %s", line.strip())
|
||||
logger.info("Pushed weekly digest to Gitea")
|
||||
return True
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("git push timed out")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error("git push error: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
def run(llm_client, date=None):
|
||||
"""Run weekly digest synthesis.
|
||||
|
||||
Reads all daily digests from the past 7 days (Mon-Sun), synthesizes
|
||||
a weekly report via DeepSeek API, and saves to ai-insights/weekly/.
|
||||
"""
|
||||
date = date or datetime.now(TZ_BEIJING)
|
||||
|
||||
# Calculate week range (Monday to Sunday)
|
||||
weekday = date.weekday() # 0=Monday, 6=Sunday
|
||||
week_start = date - timedelta(days=weekday) # This Monday
|
||||
week_end = week_start + timedelta(days=6) # This Sunday
|
||||
|
||||
logger.info(
|
||||
"Weekly digest started for W%d (%s - %s)",
|
||||
week_start.isocalendar()[1],
|
||||
week_start.strftime("%Y-%m-%d"),
|
||||
week_end.strftime("%Y-%m-%d")
|
||||
)
|
||||
|
||||
# Read all daily digests
|
||||
digests = read_week_digests(week_start, week_end)
|
||||
|
||||
if not digests:
|
||||
logger.info("No daily digests found for this week, skipping")
|
||||
return None, 0
|
||||
|
||||
logger.info("Found %d daily digests to synthesize", len(digests))
|
||||
|
||||
# Build prompt
|
||||
user_prompt = build_weekly_prompt(digests)
|
||||
if user_prompt is None:
|
||||
logger.error("Failed to build weekly prompt")
|
||||
return None, 0
|
||||
|
||||
# Call DeepSeek API
|
||||
raw_response = llm_client.ask(
|
||||
system_prompt=WEEKLY_SYSTEM_PROMPT,
|
||||
user_prompt=user_prompt,
|
||||
temperature=0.5
|
||||
)
|
||||
|
||||
if not raw_response:
|
||||
logger.error("Empty response from DeepSeek API")
|
||||
return None, 0
|
||||
|
||||
# Collect all unique tags from the week
|
||||
all_tags = set()
|
||||
for _, _, tags in digests:
|
||||
all_tags.update(tags)
|
||||
# Add default weekly tags
|
||||
default_tags = ["灵感收集器", "每周总结"]
|
||||
weekly_tags = default_tags + sorted(all_tags)
|
||||
|
||||
# Format output
|
||||
content = format_weekly_digest(week_start, week_end, raw_response, weekly_tags)
|
||||
|
||||
# Save to weekly directory
|
||||
output_dir = get_output_dir("weekly")
|
||||
week_start_str = week_start.strftime("%Y-%m-%d")
|
||||
filename = f"W{week_start_str}_digest.md"
|
||||
filepath = os.path.join(output_dir, filename)
|
||||
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
logger.info(
|
||||
"Weekly digest written to %s | %d digests | %d chars analysis | %d tags",
|
||||
filepath, len(digests), len(raw_response), len(weekly_tags)
|
||||
)
|
||||
|
||||
# Auto-push to Gitea
|
||||
git_push_weekly(filepath)
|
||||
|
||||
return filepath, len(digests)
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI entry point."""
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
||||
)
|
||||
|
||||
try:
|
||||
secrets = load_secrets()
|
||||
except FileNotFoundError as e:
|
||||
print(e)
|
||||
sys.exit(1)
|
||||
|
||||
llm_client = DeepSeekClient(
|
||||
api_key=secrets["deepseek_api_key"],
|
||||
model=secrets.get("deepseek_model", "deepseek-chat")
|
||||
)
|
||||
|
||||
filepath, count = run(llm_client)
|
||||
if filepath:
|
||||
print(f"Done: {filepath} ({count} digests synthesized)")
|
||||
else:
|
||||
print("No digests found, skipped")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user