xiaowei-system/scripts/update_profile.py

358 lines
12 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.

#!/usr/bin/env python3
"""
update_profile.py — 从 journal 增量更新用户画像
=========================================
用法:
python3 update_profile.py # 增量更新
python3 update_profile.py --full # 全量重新分析
python3 update_profile.py --dry # 只输出,不写入
依赖soulful_core.py同目录
"""
import json, os, re, sys
from datetime import datetime, timezone
from collections import Counter
HERMES = os.path.expanduser("~/.hermes")
D = HERMES + "/soulful"
JOURNAL_FILE = HERMES + "/daemon/journal.jsonl"
# 情绪词库
EMOTION_WORDS = {
"positive": ["开心", "高兴", "顺利", "", "完美", "感谢", "不错", "很好", "解决了", "成功了", "搞定", "舒服"],
"negative": ["", "", "压力", "焦虑", "沮丧", "失望", "崩溃", "", "头疼", "", "搞不定", "失败", "错误"],
"focus": ["专注", "", "", "deadline", "紧急", "重要"],
}
# 目标关键词
GOAL_KEYWORDS = ["研究", "", "", "修复", "完成", "测试", "上线", "调研", "计划", "推进", "同步"]
# 沟通风格判断(基于句长)
def detect_communication_style(summaries: list) -> str:
if not summaries:
return "简洁"
total_len = sum(len(s) for s in summaries)
avg_len = total_len / len(summaries)
if avg_len < 30:
return "简洁直接"
elif avg_len < 60:
return "简洁"
else:
return "详细"
def load_journal(n: int = 100) -> list:
"""加载最近 N 条 journal 条目"""
if not os.path.exists(JOURNAL_FILE):
return []
with open(JOURNAL_FILE) as f:
lines = f.readlines()
entries = [json.loads(l) for l in lines[-n:] if l.strip()]
return entries[::-1] # 倒序,最新的在前
def extract_goals(entries: list) -> list:
"""从 journal 中提取当前目标(通过关键词+主题判断)"""
goals = []
recent_topics = []
for e in entries:
summary = e.get("summary", "")
event_type = e.get("type", "")
# 任务完成类型的事件通常是目标
if event_type in ("solve", "learn", "skill", "config"):
# 提取关键动作
if "织忆" in summary:
goals.append("织忆系统优化")
if "ColaOS" in summary or "page-agent" in summary:
goals.append("ColaOS研究")
if "skill" in summary.lower():
goals.append("技能管理")
if "daemon" in summary.lower():
goals.append("Daemon自动化")
recent_topics.append(summary[:50])
# 去重保留顺序
seen = set()
unique_goals = []
for g in goals:
if g not in seen:
seen.add(g)
unique_goals.append(g)
return unique_goals[:5]
def extract_emotional_state(entries: list) -> tuple:
"""从 journal 中判断情绪状态"""
pos_count = 0
neg_count = 0
notes = []
for e in entries[-20:]: # 只看最近20条
summary = e.get("summary", "")
for w in EMOTION_WORDS["positive"]:
if w in summary:
pos_count += 1
for w in EMOTION_WORDS["negative"]:
if w in summary:
neg_count += 1
notes.append(summary[:60])
if neg_count > pos_count * 2:
state = "tired"
elif pos_count > neg_count:
state = "good"
else:
state = "neutral"
return state, notes[-3:]
def detect_events_for_hearttraces(entries: list) -> list:
"""从 journal 中检测值得写入心迹的事件"""
events = []
for e in entries:
summary = e.get("summary", "")
# 检测重要共同经历
if "织忆" in summary and "修好" in summary:
events.append({
"content": "我们一起修好了织忆失忆问题2026-06-25",
"tags": ["成长", "织忆", "共同经历"],
"importance": 5,
"type": "moment"
})
if "ColaOS" in summary and "研究" in summary:
events.append({
"content": "牧尘开始研究 ColaOS寻找织忆 Soulful 改造方向",
"tags": ["ColaOS", "研究"],
"importance": 4,
"type": "moment"
})
# 检测情绪表达
for w in EMOTION_WORDS["negative"]:
if w in summary:
events.append({
"content": f"牧尘表达了{w}{summary[:40]}",
"tags": ["情绪", w],
"importance": 3,
"type": "signal"
})
break
return events
# ====== Phase 5: 自动牵挂识别 ======
import re as re_module
from datetime import timedelta
# 牵挂识别模式
CARE_PATTERNS = [
# 显式提醒型
(re_module.compile(r'记得\s*(.+)'), "记得"),
(re_module.compile(r'别忘了\s*(.+)'), "别忘"),
(re_module.compile(r'别忘\s*(.+)'), "别忘"),
(re_module.compile(r'回头\s*(.+)'), "回头"),
(re_module.compile(r'等有空\s*(.+)'), "等有空"),
(re_module.compile(r'待办[:]\s*(.+)'), "待办"),
(re_module.compile(r'TODO[:]\s*(.+)'), "TODO"),
(re_module.compile(r'\[ \]\s*(.+)'), "checkbox"),
# 计划型
(re_module.compile(r'(?:要|得|该)\s*(.{5,30}?)(?:的话|的话|的时候|时)?'), "计划"),
(re_module.compile(r'处理\s*(.+)'), "处理"),
(re_module.compile(r'做一下\s*(.+)'), "做一下"),
]
# 停用词(不作为牵挂)
CARE_STOPWORDS = {"什么", "为什么", "怎么", "如何", "多少", "哪个", "这里", "那里",
"今天", "明天", "昨天", "现在", "刚才", "刚才的"}
def extract_cares_from_journal(entries: list = None, dry: bool = True) -> list:
"""从 journal 中自动识别牵挂,自动添加到 CaresQueue
Args:
entries: journal 条目列表None 则自动加载
dry: True=仅返回不写入False=写入 CaresQueue
"""
if entries is None:
entries = load_journal(100)
found_cares = []
for e in entries:
summary = e.get("summary", "")
details = e.get("details", "")
full_text = f"{summary} {details}"
for pattern, care_type in CARE_PATTERNS:
matches = pattern.findall(full_text)
for raw_text in matches:
# 清理
text = raw_text.strip().rstrip(".,,。!?")
if len(text) < 4 or len(text) > 80:
continue
# 停用词过滤
if any(sw in text for sw in CARE_STOPWORDS):
continue
# 去除动作词前缀
text = re_module.sub(r'^(记得|别忘|要|得|该|处理|做)\s*', '', text)
found_cares.append({
"content": text,
"type": care_type,
"source": summary[:50],
})
# 去重(内容相似度匹配,公共前缀 > 10 字符相同视为重复)
unique = []
for c in found_cares:
is_dup = any(
c["content"][:15] == u["content"][:15]
for u in unique
)
if not is_dup:
unique.append(c)
if dry:
return unique
# 写入 CaresQueue
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from soulful_core import CaresQueue
cq = CaresQueue()
existing = cq.pending()
added = []
# 已有牵挂的前15字符
existing_prefixes = {c["content"][:15] for c in existing}
for care in unique:
if care["content"][:15] in existing_prefixes:
continue
# follow_up_date = 3天后
follow_up = (datetime.now() + timedelta(days=3)).strftime("%Y-%m-%d")
care_id = cq.add(
content=care["content"],
context=f"[自动识别来自:{care['source']}]",
follow_up_date=follow_up,
tags=["自动识别", care["type"]]
)
added.append(care_id)
return added
def main():
dry = "--dry" in sys.argv
full = "--full" in sys.argv
print(f"[update_profile] {'[DRY]' if dry else ''} 开始更新用户画像...")
# 加载 journal
entries = load_journal(200 if full else 100)
print(f" 读取 journal: {len(entries)}")
if not entries:
print(" 无 journal 数据,退出")
return
# 提取各项
summaries = [e.get("summary", "") for e in entries]
style = detect_communication_style(summaries)
goals = extract_goals(entries)
emotion, emotion_notes = extract_emotional_state(entries)
print(f" 沟通风格: {style}")
print(f" 当前目标: {goals}")
print(f" 情绪状态: {emotion}")
if dry:
print(" [DRY] 不写入,仅输出")
return
# 更新画像
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from soulful_core import UserProfile, HeartTraces
up = UserProfile()
# 更新沟通风格
if not up.get().get("communication_style") or full:
up.update({"communication_style": style})
# 更新目标(合并去重)
if goals:
existing_goals = up.get().get("current_goals", [])
merged = list(dict.fromkeys(existing_goals + goals))[:10]
up.update({"current_goals": merged})
# 更新情绪(只有更严重才降级,不轻易升级)
current = up.get().get("emotional_state", {}).get("current", "unknown")
if emotion == "tired" or current in ("tired",):
up.set_emotional_state(emotion, "; ".join(emotion_notes[:2]) if emotion_notes else "")
print(f" ✓ 画像已更新")
# 检测心迹事件
ht = HeartTraces()
current_count = ht.count()
events = detect_events_for_hearttraces(entries)
# 写入新心迹(只写入之前没有的)
for ev in events:
# 简单去重:检查最近 10 条是否有相同内容
recent = ht.recent(n=10)
duplicate = any(ev["content"][:30] in r.get("content", "") for r in recent)
if not duplicate:
ht.record(ev["content"], ev["tags"], ev["importance"], trace_type=ev["type"])
print(f" ✦ 心迹: {ev['content'][:50]}")
new_count = ht.count()
if new_count > current_count:
print(f" ✓ 心迹新增 {new_count - current_count}")
else:
print(f" ✓ 无新增心迹({new_count} 条)")
# ── P1: Soulful → 织忆 ──────────────────────────────────
profile = up.get()
style = profile.get("communication_style", style) # 用 fresh 检测值优先
goals = profile.get("current_goals", goals)
emotion = profile.get("emotional_state", {}).get("current", emotion)
# 变更检测:只有画像发生变化才写入织忆
last_hash_file = os.path.join(HERMES, "soulful", ".last_zhiyi_hash")
current_hash = f"{style}|{emotion}|{','.join(goals[:3])}"
last_hash = ""
if os.path.exists(last_hash_file):
with open(last_hash_file) as f:
last_hash = f.read().strip()
if current_hash != last_hash:
content = f"用户画像更新: 风格={style} 情绪={emotion} 目标={', '.join(goals[:3])}"
try:
zhiyi_write(content, "distilled")
with open(last_hash_file, 'w') as f:
f.write(current_hash)
print(f" ✓ 画像 → 织忆")
except Exception as e:
print(f" ⚠ 织忆写入失败: {e}")
else:
print(f" - 画像无变化,跳过织忆写入")
# ── P1 End ──────────────────────────────────────────────
def zhiyi_write(content: str, category: str = "distilled"):
"""写入织忆http 直连)"""
import urllib.request, json
payload = json.dumps({
"content": content,
"category": category,
"agent_id": "a06"
}).encode()
req = urllib.request.Request(
"http://localhost:7821/api/v1/commit",
data=payload,
headers={"X-API-Key": "zhiyi-dev-key-2026", "Content-Type": "application/json"}
)
urllib.request.urlopen(req, timeout=5)
if __name__ == "__main__":
main()