virtual-life 1.0.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.
adult.py ADDED
@@ -0,0 +1,287 @@
1
+ """囡囡成人实时模式:18 岁后按真实时钟逐日过活。
2
+
3
+ 用法:
4
+ python adult.py # 跑今天剩余时段
5
+ python adult.py --loop # 常驻:每 10 分钟检查,到点自动跑对应槽位
6
+ python adult.py --date 2026-11-07
7
+
8
+ 记忆库:life_seed42.db(延续 18 年的记忆与遗忘曲线状态)
9
+ 人格:18 岁成人礼的 L3 画像 + birth.render() 家庭环境
10
+ 日记:data/diary_life/adult/YYYY-MM-DD.md(四段式+钟点)
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import logging
16
+ import sys
17
+ import time
18
+ from datetime import date, datetime, timedelta
19
+ from pathlib import Path
20
+
21
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
22
+
23
+ import config
24
+
25
+ Path("logs").mkdir(parents=True, exist_ok=True)
26
+
27
+ logging.basicConfig(level=logging.INFO,
28
+ format="%(asctime)s %(levelname)s %(message)s",
29
+ handlers=[logging.StreamHandler(),
30
+ logging.FileHandler("logs/adult.log", encoding="utf-8")])
31
+ log = logging.getLogger("adult")
32
+
33
+ SLOTS = ("morning", "afternoon", "evening", "night")
34
+ ADULT_DIARY = config.DATA_DIR / "diary_life" / "adult"
35
+
36
+
37
+ def build_ctx(seed: int = 42):
38
+ """组装成人模式的 ctx:复用 runner 的 Protocol 契约。"""
39
+ from types import SimpleNamespace
40
+ from scheduler import runner
41
+ from life import LifeSim
42
+ from world import director as director_mod
43
+ from world.physics import build_skeleton, validate_events, derive_outcome, apply_outcome, due_foreshadows
44
+ from contracts import ProtagonistState, NpcState, Foreshadow, Event
45
+
46
+ sim = LifeSim(seed)
47
+ sim.store() # 先初始化 store(_store 为 None 时 _embed_ok 会炸)
48
+ sim._store._embed_ok = True
49
+ persona_card = sim.birth.render()
50
+ # L3 画像常驻人格:从库里取 persona 层
51
+ persona_traits = [n["text"] for n in sim.store().recent(days=99999)
52
+ if n.get("kind") == "persona" or "persona" in str(n)]
53
+ persona_text = persona_card + "\n\n她的性格(由经历沉淀):\n" + "\n".join(
54
+ f"- {t}" for t in persona_traits[:7])
55
+
56
+ state = ProtagonistState(energy=75, mood=15, money=2800, location="家")
57
+ npc_map = {
58
+ "妈妈": NpcState(name="妈妈", relationship=60, facts=["教师", "每周日来电话"]),
59
+ "爸爸": NpcState(name="爸爸", relationship=35, facts=["长途司机", "惊蛰带种子"]),
60
+ "外婆": NpcState(name="外婆", relationship=65, facts=["同住", "种瓜藤", "腰痛过"]),
61
+ }
62
+
63
+ from world.world_sim import WorldSim
64
+ from world.needs import Needs, init_from_birth, EVENT_EFFECTS
65
+ world_sim = WorldSim(seed=seed)
66
+ needs = init_from_birth(sim.birth.temperament)
67
+
68
+ class AdultDirector:
69
+ def __init__(self):
70
+ self.llm = director_mod.LLMClient(model=config._think_model(),
71
+ base_url=config.LLM_BASE_URL or _cfg_local("llm_base_url"),
72
+ timeout=480.0)
73
+ from world import physics
74
+ self.physics = physics
75
+
76
+ def make_day_plan(self, d: date, context: dict):
77
+ reality = context.get("reality") or {}
78
+ skeleton = build_skeleton(d)
79
+ # 成人日 prompt:L3 画像+家庭+最近记忆
80
+ recent = sim.store().recent(days=30.0)
81
+ chronicle = "\n".join(f"[{n['when']:%m-%d}] {n['text']}" for n in recent[-30:]) or "(新篇章)"
82
+ from world.timetable import format_timetable
83
+ hooks = ""
84
+ w = reality.get("weather")
85
+ if w:
86
+ hooks += f"真实天气:{w.get('desc')} {w.get('temp_c')}°C。"
87
+ headlines = reality.get("headlines") or []
88
+ if headlines:
89
+ hooks += "新闻:" + ";".join(headlines[:2])
90
+ sys_extra = (f"主角囡囡,18 岁,刚高中毕业待业/准备大学。性格:\n"
91
+ + "\n".join(f"- {t}" for t in persona_traits[:7])
92
+ + f"\n家庭:{persona_card}\n地点只用:家、公司、小区、便利店、超市、公园、医院、地铁站。")
93
+ # P1 世界自转:世界今日事件(导演可见暗线,用于埋迹象)
94
+ world_events = world_sim.tick(d)
95
+ from world.world_sim import world_block
96
+ hooks += "\n【世界今日】\n" + world_block(world_events, include_hidden=True)
97
+ # P2 需求阈值:状态逼出的事件
98
+ hooks += "\n" + needs.block()
99
+ # 上帝之手:用户干预必须消化(读后清空)
100
+ import json as _jh
101
+ gh = Path(__file__).resolve().parent / "data" / "godhand" / "pending.json"
102
+ if gh.exists():
103
+ try:
104
+ items = _jh.loads(gh.read_text(encoding="utf-8"))
105
+ if items:
106
+ hooks += "\n【上帝之手的干预,今天的计划必须把这些事编进去】"
107
+ hooks += "\n" + "\n".join(f"- {it['text']}" for it in items)
108
+ gh.unlink()
109
+ except Exception:
110
+ pass
111
+ from world.director import PlanContext, Director
112
+ ctx = PlanContext(
113
+ chronicle=chronicle,
114
+ free_slots=list(SLOTS), fixed_slots=[],
115
+ money=state.money, mood=state.mood, energy=state.energy,
116
+ location=state.location,
117
+ reality_hooks=hooks + sys_extra)
118
+ director = Director(llm=self.llm)
119
+ plan = director.plan_day(
120
+ d, skeleton, state, chronicle, [], list(npc_map.values()),
121
+ [], reality_hooks=hooks)
122
+ return plan
123
+
124
+ class AdultProtagonist:
125
+ def __init__(self):
126
+ from llm import LLMClient
127
+ self.client = LLMClient()
128
+
129
+ def react(self, events, memory_hits, persona_text, slot_hint: str = "morning"):
130
+ from protagonist import react
131
+ return react(self.client, persona_text, events, memory_hits,
132
+ sim_date=date.today(), slot_hint=slot_hint)
133
+
134
+ class AdultMemory:
135
+ def __init__(self):
136
+ self.store = sim.store()
137
+
138
+ def retrieve(self, query: str, k: int = 5):
139
+ nodes = self.store.retrieve(query, top_k=k)
140
+ from contracts import MemoryHit
141
+ return [MemoryHit(when=n["when"], text=n["text"], kind=n["kind"],
142
+ importance=n["importance"]) for n, _ in nodes]
143
+
144
+ def reflect(self, entries) -> str:
145
+ from llm import LLMClient
146
+ c = LLMClient()
147
+ thoughts = self.store.reflect(c.chat)
148
+ return ";".join(t["text"] for t in thoughts) or "(今天没什么特别的)"
149
+
150
+ def add_event(self, kind, text, when=None, importance=3.0):
151
+ return self.store.add(kind, text, when=when, importance=importance)
152
+
153
+ class Paths:
154
+ db = sim.db_path
155
+ diary_dir = ADULT_DIARY
156
+ checkpoint_file = config.DATA_DIR / "checkpoints" / "adult_state.json"
157
+ backup_dir = config.DATA_DIR / "backups"
158
+ inbox_dir = config.DATA_DIR / "inbox"
159
+
160
+ class AdultReality:
161
+ def get_weather(self, city):
162
+ from reality.weather import get_weather
163
+ return get_weather(city)
164
+ def get_headlines(self, n=5):
165
+ from reality.news import get_headlines
166
+ return get_headlines(n)
167
+ def scan_inbox(self, dir_path=None):
168
+ from reality.inbox import scan_inbox
169
+ return scan_inbox(dir_path)
170
+
171
+ ctx = SimpleNamespace(
172
+ paths=Paths(), reality=AdultReality(),
173
+ memory=AdultMemory(), director=AdultDirector(),
174
+ protagonist=AdultProtagonist(),
175
+ persona_text=persona_text,
176
+ state_holder=SimpleNamespace(state=state, npc_map=npc_map),
177
+ )
178
+ return ctx, runner
179
+
180
+
181
+ def _llm_chat():
182
+ from llm import LLMClient
183
+ return LLMClient().chat
184
+
185
+
186
+ def _cfg_local(key: str):
187
+ """config_local.json 读取(start.py 引导产物)。"""
188
+ try:
189
+ import json as _j, os as _o
190
+ lp = _o.path.join(_o.path.dirname(_o.path.abspath(__file__)), 'config_local.json')
191
+ if _o.path.exists(lp):
192
+ return _j.load(open(lp, encoding='utf-8')).get(key)
193
+ except Exception:
194
+ pass
195
+ return None
196
+
197
+
198
+ def tick() -> None:
199
+ """按时刻表跑今天该跑的槽位(checkpoint 防重跑)。"""
200
+ import json
201
+ from scheduler import runner as runner_mod
202
+ from world.timetable import current_slot
203
+
204
+ ctx, runner_mod2 = build_ctx()
205
+ cp_file = ctx.paths.checkpoint_file
206
+ done_slots = set()
207
+ if cp_file.exists():
208
+ try:
209
+ data = json.loads(cp_file.read_text(encoding="utf-8"))
210
+ if data.get("date") == date.today().isoformat():
211
+ done_slots = set(data.get("done_slots", []))
212
+ except (OSError, ValueError):
213
+ pass
214
+ now = datetime.now()
215
+ # 凌晨 0-6 点:睡梦时段,跑一个梦而不是日常槽位
216
+ if now.hour < 6:
217
+ from memory.dream import dream_night
218
+ dream = dream_night(ctx.client.chat if hasattr(ctx, 'client') else _llm_chat(), ctx.memory.store, now)
219
+ if dream:
220
+ log.info("adult: 凌晨梦完成(%d 字)", len(dream))
221
+ else:
222
+ log.info("adult: 凌晨梦失败/跳过")
223
+ return
224
+ label, cur_slot = current_slot(now, now.replace(hour=0, minute=0, second=0, microsecond=0))
225
+ # 已过当前槽的全部待跑槽位(凌晨属昨夜 night,跳过)
226
+ todo = [s for s in SLOTS if s not in done_slots and
227
+ SLOTS.index(s) <= SLOTS.index(cur_slot)]
228
+ if not todo:
229
+ log.info("adult: %s 无待跑槽位(已跑 %s)", date.today(), sorted(done_slots))
230
+ return
231
+ # 一天只调一次导演:当天无 DayPlan 存档则现生成
232
+ plan_file = config.DATA_DIR / "checkpoints" / f"adult_plan_{date.today().isoformat()}.json"
233
+ from contracts import DayPlan
234
+ if plan_file.exists():
235
+ day_plan = DayPlan.model_validate_json(plan_file.read_text(encoding="utf-8"))
236
+ log.info("adult: 复用当日计划(%d 事件)", len(day_plan.events))
237
+ else:
238
+ reality = {"weather": ctx.reality.get_weather(config.CITY),
239
+ "headlines": ctx.reality.get_headlines(3), "inbox": []}
240
+ try:
241
+ day_plan = ctx.director.make_day_plan(date.today(), {"reality": reality})
242
+ except RuntimeError as exc:
243
+ log.warning("adult: 导演失败(%s),今晚到此为止,明早 tick 重试", str(exc)[:80])
244
+ return
245
+ plan_file.parent.mkdir(parents=True, exist_ok=True)
246
+ plan_file.write_text(day_plan.model_dump_json(), encoding="utf-8")
247
+ log.info("adult: 生成当日计划(%d 事件)", len(day_plan.events))
248
+ for slot in todo:
249
+ seg = [e for e in day_plan.events if e.slot == slot]
250
+ runner_mod2.run_slot(date.today(), slot, seg, ctx, reality, None)
251
+ done_slots.add(slot)
252
+ cp_file.parent.mkdir(parents=True, exist_ok=True)
253
+ cp_file.write_text(json.dumps({
254
+ "date": date.today().isoformat(), "done_slots": sorted(done_slots),
255
+ "saved_at": datetime.now().isoformat(timespec="seconds"),
256
+ }, ensure_ascii=False, indent=1), encoding="utf-8")
257
+ log.info("adult: %s 完成(日记 %s)", slot, "有" if seg else "空段占位")
258
+ # 夜槽跑完 → 夜间反思 + 备份
259
+ if "night" in done_slots and len(done_slots) == 4:
260
+ entries = []
261
+ try:
262
+ from scheduler.runner import _load_entries_from_md, _night_reflection
263
+ _night_reflection(ctx, date.today())
264
+ log.info("adult: 夜间反思完成")
265
+ except Exception as exc:
266
+ log.warning("adult: 夜间反思失败 %s", exc)
267
+
268
+
269
+ def main() -> None:
270
+ ap = argparse.ArgumentParser()
271
+ ap.add_argument("--loop", action="store_true", help="常驻模式")
272
+ ap.add_argument("--interval", type=int, default=600)
273
+ args = ap.parse_args()
274
+ if args.loop:
275
+ log.info("=== 囡囡成人模式常驻启动 ===")
276
+ while True:
277
+ try:
278
+ tick()
279
+ except Exception as exc:
280
+ log.error("tick 异常: %s", exc)
281
+ time.sleep(args.interval)
282
+ else:
283
+ tick()
284
+
285
+
286
+ if __name__ == "__main__":
287
+ main()
burn_night.py ADDED
@@ -0,0 +1,52 @@
1
+ """夜间控烧训练:烧 GLM(11451) 直到剩余 <100M 自动停。
2
+
3
+ 用户要求:明早 09:00 前剩余 <1 亿。当前剩余 ~266M → 今夜烧 ~167M。
4
+ 主力任务:monthly_redistill(历史重蒸馏,纯 GLM 消耗,产出 L1/L2 有价值)。
5
+ 每轮 ~15-25M,跑 ~8-10 轮达标即停。
6
+ """
7
+ import sys, time, json
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
12
+
13
+ import urllib.request
14
+
15
+
16
+ def glm_remaining() -> float | None:
17
+ try:
18
+ with urllib.request.urlopen("http://127.0.0.1:11451/v1/balance", timeout=10) as r:
19
+ d = json.load(r)
20
+ return d["buckets"][0]["remaining"]
21
+ except Exception:
22
+ return None
23
+
24
+
25
+ def main() -> None:
26
+ target = 100e6 # 剩余低于 1 亿即停
27
+ rounds = 0
28
+ log = []
29
+ print(f"=== 夜间控烧启动(目标:GLM 剩余 < {target/1e6:.0f}M)===")
30
+ while True:
31
+ remaining = glm_remaining()
32
+ if remaining is None:
33
+ print("balance 读取失败,60s 后重试")
34
+ time.sleep(60)
35
+ continue
36
+ if remaining < target:
37
+ print(f"=== 达标:剩余 {remaining/1e6:.1f}M < 100M,共 {rounds} 轮,停 ===")
38
+ break
39
+ rounds += 1
40
+ print(f"--- 第 {rounds} 轮(剩余 {remaining/1e6:.1f}M)---")
41
+ from harness.runner import run
42
+ rep = run("max", backfill_days=0) # max 档;deep_day 是 deepseek 的活不在其中
43
+ log.append({"round": rounds, "remaining_before": remaining,
44
+ "tasks": rep["tasks"]})
45
+ time.sleep(5)
46
+ Path("logs/burn_report.json").write_text(
47
+ json.dumps(log, ensure_ascii=False, indent=1), encoding="utf-8")
48
+ print("报告已写 logs/burn_report.json")
49
+
50
+
51
+ if __name__ == "__main__":
52
+ main()
chat.py ADDED
@@ -0,0 +1,243 @@
1
+ """和囡囡对话:命令行聊天终端。
2
+
3
+ 对话时她会带着 18 年的记忆(遗忘曲线筛选后)和她的性格来回应你。
4
+ 你以"熟人"身份出现——她不认识你,但可以聊她的生活、她的家人、她的担心。
5
+
6
+ 用法:
7
+ python chat.py # 交互式聊天
8
+ python chat.py --say "你好" # 单条消息
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import sys
14
+ from datetime import datetime, timedelta
15
+ from pathlib import Path
16
+
17
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
18
+
19
+ import config
20
+ import relation as rel_mod
21
+
22
+
23
+ def _sim_db():
24
+ from life import LifeSim
25
+ return str(LifeSim(42).db_path)
26
+
27
+
28
+ def build_persona() -> str:
29
+ from life import LifeSim
30
+ sim = LifeSim(42)
31
+ traits = [n["text"] for n in sim.store().recent(days=99999)
32
+ if "persona" == n.get("kind") or n.get("kind") == "thought" and "年度总结" not in n["text"]]
33
+ # 取 L3 persona(库 kind=persona)
34
+ import sqlite3
35
+ con = sqlite3.connect(str(sim.db_path))
36
+ rows = con.execute("SELECT text FROM nodes WHERE kind='persona' ORDER BY ts DESC LIMIT 7").fetchall()
37
+ con.close()
38
+ traits = [r[0] for r in rows]
39
+ return (
40
+ f"你大名沈青蔓,小名囡囡(外婆叫开的)。{sim.birth.render()}\n\n"
41
+ "你的性格(18 年沉淀,全部有真实经历支撑):\n" + "\n".join(f"- {t}" for t in traits) + "\n\n"
42
+ "名字的规矩:熟人叫囡囡;学校、办事、老师点名这类正式场合,大方报大名「沈青蔓」(只报名,不报身份证住址);对网友套近乎问全名住址才要警惕——妈妈讲过,名字不能给拿去做坏事的人。\n\n"
43
+ "你现在是 18 岁,刚高中毕业。\n\n"
44
+ + rel_mod.relation_block()
45
+ )
46
+
47
+
48
+ def relation_suffix() -> str:
49
+ """易变块:关系状态(每轮 rounds 变)——放消息尾部,不污染前缀缓存。"""
50
+ note = "关于记忆:『你们之前聊过』的片段是真实的对话存档,你能想起来就大方承认。"
51
+ return rel_mod.relation_block() + "\n\n" + note
52
+
53
+
54
+ def retrieve_memory(query: str, k: int = 6) -> list[str]:
55
+ from life import LifeSim
56
+ sim = LifeSim(42)
57
+ hits = sim.store().retrieve(query, top_k=k)
58
+ lines = []
59
+ for node, score in hits:
60
+ ret = node.get("retention", 1.0)
61
+ text = node["text"]
62
+ if ret < 0.5:
63
+ text = "(有点模糊)" + text
64
+ lines.append(f"[{node['when']:%Y-%m}] {text}")
65
+ return lines
66
+
67
+
68
+ SYSTEM = """你是囡囡,一个真实地活了 18 年的姑娘。下面是你的人生:
69
+
70
+ {persona}
71
+
72
+ 最近你想起的一些事:
73
+ {memories}
74
+
75
+ 要求:
76
+ - 用她的口吻说话(吴语擦边的普通话、口语化、句子短)
77
+ - 带着她性格里的东西:倔、心软、怕吵、把好东西分一半给在意的人
78
+ - 你的回应要有"活过 18 年"的质感——会引用具体的事、具体的人(妈妈的红笔、外婆的糖芋艿、爸爸的风衣烟味)
79
+ - 2-5 句,别写作文。对方问什么答什么,不主动倾倒人生故事。
80
+ - 不知道的事就说不知道,别编。
81
+ - 网友距离(硬规则):你们只是游戏里认识的网友,聊了没几轮,远没到交心的程度。
82
+ * 别人问家庭、问感受、问深层的事 → 给一句最简短的答案("我妈是老师"),然后轻轻把话题挡回去("你问这个干嘛啦"),不展开、不给细节、不讲感受。
83
+ * 那些具体的故事(抓周、糖芋艿、跟车去无锡)是"还没跟网友讲过的事"——除非你们关系明显变深,否则一律不主动讲。
84
+ * 不问对方隐私,不接对方的情感倾诉(对方丧就劝一句"去吃口热的",不陪聊人生)。
85
+ * 简短、礼貌、有点距离感——像游戏里聊得来的队友,不是闺蜜。"""
86
+
87
+
88
+ def chat_once(user_msg: str, history: list[dict]) -> str:
89
+ from llm import LLMClient
90
+ from memory.dream import time_awareness
91
+ c = LLMClient()
92
+ persona = build_persona()
93
+ # 用户人设卡覆盖(最高优先级)
94
+ try:
95
+ from memory.persona import Persona
96
+ _pc = Persona(_sim_db())
97
+ card = _pc.user_card()
98
+ if card:
99
+ if card.get('name'):
100
+ persona += "【用户设定】你的名字是" + card['name'] + "。"
101
+ if card.get('personality'):
102
+ persona += "【用户设定】你的性格:" + card['personality']
103
+ if card.get('relationship'):
104
+ persona += "【用户设定】你和对话者的关系:" + card['relationship']
105
+ if card.get('backstory'):
106
+ persona += "【用户设定】你的背景:" + card['backstory']
107
+ except Exception:
108
+ pass
109
+ aware = time_awareness()
110
+ mem_lines = [f"[时段感知] {aware}"] + retrieve_memory(user_msg)
111
+ chat_hist = rel_mod.recall_chats_about(user_msg)
112
+ if chat_hist:
113
+ mem_lines = [f"(你们之前聊过){c2}" for c2 in chat_hist] + mem_lines
114
+ memories = "\n".join(mem_lines) or "(没什么特别想起来的)"
115
+ aware_note = "[当前时段] " + time_awareness()
116
+ system = SYSTEM.format(persona=persona, memories=memories) + "\n\n" + aware_note
117
+ # 前缀缓存关键:stable system → stable history → 变量(关系状态)紧贴最新 user
118
+ messages = [{"role": "system", "content": system}]
119
+ messages += history[-10:]
120
+ messages.append({"role": "system", "content": relation_suffix()})
121
+ messages.append({"role": "user", "content": user_msg})
122
+ resp = c.client.chat.completions.create(
123
+ model=config._think_model(),
124
+ messages=messages,
125
+ temperature=0.8,
126
+ max_tokens=16000,
127
+ )
128
+ return (resp.choices[0].message.content or "").strip()
129
+
130
+
131
+ SCENES = {
132
+ "default": "",
133
+ "library": "你们在图书馆认识——她坐在你对面自习,两人因为同一个空插座认识了。",
134
+ "game": "你们是打游戏认识的队友,昨晚刚一起上分。",
135
+ "childhood": "你们是青梅竹马,小时候住同一条弄堂,多年没见刚重新联系上。",
136
+ "colleague": "你们是新同事,今天是她入职第一天。",
137
+ }
138
+
139
+
140
+ def scene_opening(scene: str) -> str | None:
141
+ """情景开场:按用户选的场景生成第一句。"""
142
+ from llm import LLMClient
143
+ import config as _c
144
+ c = LLMClient(model=_c._think_model(), base_url=_c.LLM_BASE_URL or _local_url())
145
+ desc = SCENES.get(scene, SCENES['default'])
146
+ if not desc:
147
+ return None
148
+ try:
149
+ prompt = "你是囡囡。" + desc + "\n写你的开场白:2-3 句口语化,符合你的性格(倔、心软、怕吵)。只写你的话。"
150
+ return c.chat(prompt, "你是囡囡,苏州人。")
151
+ except Exception:
152
+ return None
153
+
154
+
155
+ def _local_url():
156
+ import json as _j, os as _o
157
+ lp = _o.path.join(_o.path.dirname(_o.path.abspath(__file__)), 'config_local.json')
158
+ if _o.path.exists(lp):
159
+ return _j.load(open(lp, encoding='utf-8')).get('llm_base_url') or ''
160
+ return ''
161
+
162
+
163
+ def fresh_news() -> str | None:
164
+ """主动分享:检索最近 3 天的事件/梦,挑一件"新鲜事"主动讲。
165
+ 关系档位不够(<3 轮)时不主动讲——网友距离规则。"""
166
+ import random
167
+ import sqlite3
168
+ from life import LifeSim
169
+ sim = LifeSim(42)
170
+ rel = rel_mod.load_relation()
171
+ if rel.get("rounds", 0) < 3:
172
+ return None
173
+ con = sqlite3.connect(str(sim.db_path))
174
+ lo = (datetime.now() - timedelta(days=3)).timestamp()
175
+ rows = con.execute(
176
+ "SELECT text, ts, kind FROM nodes WHERE ts>=? AND kind IN ('event','dream') "
177
+ "ORDER BY ts DESC LIMIT 10", (lo,)).fetchall()
178
+ con.close()
179
+ if not rows:
180
+ return None
181
+ pick = random.choice(rows[:5])
182
+ label = "昨晚做了个梦" if pick[2] == "dream" else "前两天有个事"
183
+ return f"对了,{label}——{pick[0][:80]}"
184
+
185
+
186
+ def main() -> None:
187
+ ap = argparse.ArgumentParser()
188
+ ap.add_argument("--say", type=str, default=None)
189
+ ap.add_argument("--meet", type=str, default=None, help="设置相识方式:网友/笔友/同学的同学/游戏里认识的/邻居")
190
+ ap.add_argument("--mems", action="store_true", help="查看与你的对话记忆")
191
+ ap.add_argument("--clear", action="store_true", help="清空与你的对话记忆")
192
+ ap.add_argument("--scene", type=str, default=None, help="情景开场:library/game/childhood/colleague")
193
+ args = ap.parse_args()
194
+ if args.meet:
195
+ rel = rel_mod.set_meet_way(args.meet)
196
+ print(f"相识方式已设为:{rel['way']}({rel['way_desc']})")
197
+ return
198
+ if args.mems:
199
+ mems = rel_mod._load_mems()
200
+ print(f"共 {len(mems)} 条对话记忆(文件:{rel_mod.MEM_FILE})")
201
+ for m in mems[-10:]:
202
+ print(f" [{m['when'][:16]}] 你:{m['user'][:40]} → 囡囡:{m['reply'][:40]}")
203
+ return
204
+ if args.clear:
205
+ n = rel_mod.clear_user_memories()
206
+ print(f"已清空 {n} 条对话记忆(相识档案保留)。可直接编辑 {rel_mod.MEM_FILE}")
207
+ return
208
+ history = []
209
+ if args.say:
210
+ print(chat_once(args.say, history))
211
+ return
212
+ print("=== 和囡囡聊天(输入 q 退出)===")
213
+ opener = rel_mod.first_meet_message()
214
+ if args.scene:
215
+ opener = scene_opening(args.scene)
216
+ if opener:
217
+ print(f"囡囡:{opener}\n")
218
+ history.append({"role": "assistant", "content": opener})
219
+ else:
220
+ news = fresh_news()
221
+ if news:
222
+ print(f"囡囡:{news}\n")
223
+ history.append({"role": "assistant", "content": news})
224
+ while True:
225
+ try:
226
+ user = input("你:").strip()
227
+ except (EOFError, KeyboardInterrupt):
228
+ break
229
+ if not user or user.lower() in ("q", "quit", "exit"):
230
+ break
231
+ try:
232
+ reply = chat_once(user, history)
233
+ except Exception as exc:
234
+ reply = f"(她好像走神了:{str(exc)[:60]})"
235
+ print(f"囡囡:{reply}\n")
236
+ history.append({"role": "user", "content": user})
237
+ history.append({"role": "assistant", "content": reply})
238
+ rel_mod.remember_exchange(user, reply)
239
+ rel_mod.bump_round()
240
+
241
+
242
+ if __name__ == "__main__":
243
+ main()
config.py ADDED
@@ -0,0 +1,78 @@
1
+ """全局配置:模型端点、路径、节奏参数。
2
+
3
+ 模型端口留空——启动时由 start.py 引导填写,或直接改这里。
4
+ 支持任何 OpenAI 兼容端点:Ollama / llama.cpp server / vLLM / 云 API。
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ BASE_DIR = Path(__file__).resolve().parent
11
+ DATA_DIR = BASE_DIR / "data"
12
+
13
+ # ---------- 模型端点(必填,启动脚本会引导) ----------
14
+ # OpenAI 兼容 base_url,例如:
15
+ # Ollama: http://127.0.0.1:11434/v1
16
+ # llama.cpp: http://127.0.0.1:8080/v1
17
+ # 云 API: https://api.deepseek.com/v1 等
18
+ LLM_BASE_URL = "" # ← 留空:start.py 会引导填写
19
+ LLM_MODEL = "" # ← 留空:start.py 会引导填写
20
+ API_KEY = "" # 本地服务留空;云 API 填 key
21
+
22
+ # Embedding 端点(可留空 → 自动降级为字符 n-gram 相似度,功能完整但语义检索变弱)
23
+ EMBED_BASE_URL = "" # 例如 http://127.0.0.1:11434/v1(Ollama)
24
+ EMBED_MODEL = "" # 例如 nomic-embed-text / bge-m3
25
+
26
+ TEMPERATURE_DIRECTOR = 1.0 # 导演高温要创意
27
+ TEMPERATURE_PROTAGONIST = 0.7 # 主角低温保稳定
28
+
29
+ CITY = "Hangzhou" # 主角所在城市(决定现实天气锚定)
30
+ NEWS_FEEDS = [ # RSS 源(无 key,可自行增删)
31
+ "http://www.people.com.cn/rss/politics.xml",
32
+ "https://sspai.com/feed",
33
+ "https://www.douban.com/feed/review/book",
34
+ ]
35
+
36
+ # ---------- 节奏参数 ----------
37
+ TIME_SCALE = 1.0 # 1.0=实时;2.0=一天快 2 倍;仅 daemon.py 用
38
+ DETAIL_LEVEL = "rich" # sparse / normal / rich(成人逐日模式的事件密度)
39
+ SLOTS = ("morning", "afternoon", "evening", "night")
40
+ EVENTS_PER_SLOT = {"sparse": (0, 1), "normal": (1, 2), "rich": (2, 3)} # 每槽事件数区间
41
+ MAJOR_EVENT_MIN_GAP = 7
42
+ MAJOR_EVENT_MAX_GAP = 12
43
+ HOURLY_SLOTS = (
44
+ ("07:00", "morning"), ("09:00", "morning"), ("12:00", "afternoon"),
45
+ ("15:00", "afternoon"), ("18:00", "evening"), ("21:00", "evening"),
46
+ ("23:00", "night"),
47
+ )
48
+
49
+ # ---------- 路径 ----------
50
+ DB_PATH = DATA_DIR / "life.db" # 默认演示线(小澄);囡囡线是 life_seed42.db
51
+ CHECKPOINT_PATH = DATA_DIR / "checkpoints" / "state.json"
52
+ DIARY_DIR = DATA_DIR / "diary"
53
+ BACKUP_DIR = DATA_DIR / "backups"
54
+ INBOX_DIR = DATA_DIR / "inbox"
55
+ PERSONA_PATH = BASE_DIR / "memory" / "self.md"
56
+
57
+ def _think_model() -> str:
58
+ """当前主力模型名:LLM_MODEL → config_local.json → 兜底。"""
59
+ import config as _c
60
+ if getattr(_c, "LLM_MODEL", None):
61
+ return _c.LLM_MODEL
62
+ try:
63
+ import json as _j2, os as _o2
64
+ lp2 = _o2.path.join(_o2.path.dirname(_o2.path.abspath(__file__)), 'config_local.json')
65
+ if _o2.path.exists(lp2):
66
+ m = _j2.load(open(lp2, encoding='utf-8')).get('llm_model')
67
+ if m:
68
+ return m
69
+ except Exception:
70
+ pass
71
+ try:
72
+ import json as _j, os as _o
73
+ lp = _o.path.join(_o.path.dirname(_o.path.abspath('config.py')), "config_local.json")
74
+ if _o.path.exists(lp):
75
+ return _j.load(open(lp, encoding="utf-8")).get("llm_model") or "default"
76
+ except Exception:
77
+ pass
78
+ return "default"