Files
inspiration-collector/analyzers/weekly_digest.py

377 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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不要代码块包裹。
## 附加素材:今日新知
过去7天的日报中凡是出现了"### 今日新知"章节的,都包含用户本周积累的新知识碎片。这些新知不属于"灵感"需要AI深挖而是用户记录下来的外部信息碎片。在周报中
- 在适当位置汇总本周新积累的知识领域
- 说明哪些知识是零散信息、哪些已形成体系
- 判断这些新知与用户长期主题(交易、写作、培训等)的潜在关联
结构参考:
### 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")
parts = date_str.split("-")
daily_dir = os.path.join(daily_root, parts[0], parts[1], parts[2])
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()