RachelNongyingLI commited on
Commit
9c6b45d
·
verified ·
1 Parent(s): 63a7b71

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +370 -0
app.py ADDED
@@ -0,0 +1,370 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ---------- 1) Imports & Config ----------
2
+ import os, uuid, json, random
3
+ from dataclasses import dataclass, field, asdict
4
+ from typing import List, Dict, Any
5
+ import gradio as gr
6
+
7
+ # 叙事后端选择: "hf" (CPU, falcon-1b-instruct) 或 "openai"
8
+ NARRATION_BACKEND = os.getenv("NARRATION_BACKEND", "hf")
9
+
10
+ # ---------- 9) Narration Backend ----------
11
+ if NARRATION_BACKEND == "hf":
12
+ from transformers import pipeline
13
+ generator = pipeline("text-generation", model="tiiuae/falcon-1b-instruct", device=-1)
14
+ def narrate(prompt:str) -> str:
15
+ out = generator(
16
+ "你是合欢宗的旁白,写狗血八卦日记,中文为主,轻混英文术语(jealousy, scandal, oath),不得露骨:\n"+prompt,
17
+ max_new_tokens=180, do_sample=True, temperature=0.9, top_p=0.95
18
+ )[0]["generated_text"]
19
+ return out[-600:].strip()
20
+ else:
21
+ from openai import OpenAI
22
+ client = OpenAI()
23
+ def narrate(prompt:str) -> str:
24
+ res = client.chat.completions.create(
25
+ model="gpt-4o-mini",
26
+ messages=[
27
+ {"role":"system","content":"你是合欢宗的旁白,要写成狗血八卦日记,中文为主,轻混英文术语(jealousy, scandal, oath),不得露骨。每次 2-4 句。"},
28
+ {"role":"user","content": prompt}
29
+ ],
30
+ temperature=0.9, max_tokens=300
31
+ )
32
+ return res.choices[0].message.content.strip()
33
+
34
+ # ---------- 2) Data Models ----------
35
+ Day = int
36
+
37
+ @dataclass
38
+ class Agent:
39
+ name: str
40
+ role: str
41
+ cultivation: int = 50
42
+ willpower: int = 50
43
+ face: int = 0
44
+ jealousy: int = 0
45
+ scandal: int = 0
46
+ memory: Dict[str, Any] = field(default_factory=dict) # private KnowledgeItems by fact_key
47
+ belief_about_others: Dict[str, Dict[str, Any]] = field(default_factory=dict) # ToM
48
+
49
+ @dataclass
50
+ class Event:
51
+ id: str
52
+ day: Day
53
+ type: str
54
+ actors: List[str]
55
+ payload: Dict[str, Any]
56
+ truth_strength: float = 1.0
57
+
58
+ @dataclass
59
+ class Observation:
60
+ event_id: str
61
+ observer: str
62
+ day: Day
63
+ mode: str # "direct" | "overheard" | "told"
64
+ noise: float
65
+ cred: float
66
+
67
+ @dataclass
68
+ class KnowledgeItem:
69
+ id: str
70
+ fact_key: str
71
+ content: Dict[str, Any]
72
+ first_seen_day: Day
73
+ last_update_day: Day
74
+ confidence: float
75
+ sources: List[Dict[str, Any]] = field(default_factory=list)
76
+ is_public: bool = False
77
+ visibility: str = "private" # "private"|"public"
78
+
79
+ @dataclass
80
+ class World:
81
+ day: Day = 0
82
+ agents: List[Agent] = field(default_factory=list)
83
+ relations: Dict[str, Dict[str, int]] = field(default_factory=dict) # [-100,100]
84
+ events: Dict[str, Event] = field(default_factory=dict)
85
+ observations: List[Observation] = field(default_factory=list)
86
+ public_board: List[KnowledgeItem] = field(default_factory=list)
87
+ rumor_level: int = 10
88
+ season: str = "normal"
89
+
90
+ def snapshot(self):
91
+ return {
92
+ "day": self.day,
93
+ "agents": [asdict(a) for a in self.agents],
94
+ "rumor_level": self.rumor_level,
95
+ "season": self.season,
96
+ "recent_public": [asdict(x) for x in self.public_board[-8:]],
97
+ }
98
+
99
+ # ---------- 3) Init ----------
100
+ ROLES = ["Saintess","Senior","Enforcer","Alchemist","ArrayMaster","Bard","Matchmaker","Shadow","Guest","Head"]
101
+
102
+ def init_world(seed=42) -> World:
103
+ random.seed(seed)
104
+ ags = [Agent(name=f"A{i+1}", role=ROLES[i]) for i in range(10)]
105
+ names = [a.name for a in ags]
106
+ rel = {u:{v:(0 if u==v else random.randint(-10,10)) for v in names} for u in names}
107
+ return World(day=0, agents=ags, relations=rel)
108
+
109
+ WORLD = init_world()
110
+
111
+ # ---------- 4) Info System: helpers ----------
112
+ def make_event(day, typ, actors, payload, truth_strength=1.0):
113
+ return Event(id=str(uuid.uuid4())[:8], day=day, type=typ, actors=actors, payload=payload, truth_strength=truth_strength)
114
+
115
+ def sample_observers(world:World, event:Event, base_p=0.2):
116
+ observers = []
117
+ for ag in world.agents:
118
+ if ag.name in event.actors: continue
119
+ # 关系平均值影响观测概率
120
+ rel = sum(world.relations[ag.name][x] for x in event.actors)/max(1,len(event.actors))
121
+ p = min(0.85, max(0.05, base_p + rel/200))
122
+ if random.random() < p:
123
+ observers.append(Observation(
124
+ event_id=event.id, observer=ag.name, day=event.day,
125
+ mode=random.choice(["direct","overheard"]), noise=random.uniform(0,0.25),
126
+ cred=random.uniform(0.6,0.9)
127
+ ))
128
+ return observers
129
+
130
+ def upsert_memory(agent:Agent, fact_key:str, content:dict, day:int, source:dict, base_conf:float):
131
+ it = agent.memory.get(fact_key)
132
+ if it is None:
133
+ it = KnowledgeItem(
134
+ id=str(uuid.uuid4())[:8], fact_key=fact_key, content=content,
135
+ first_seen_day=day, last_update_day=day, confidence=base_conf,
136
+ sources=[source], is_public=False, visibility="private"
137
+ )
138
+ agent.memory[fact_key] = it
139
+ else:
140
+ c0, c1 = it.confidence, base_conf
141
+ it.confidence = 1 - (1-c0)*(1-c1) # 概率互补合并
142
+ it.last_update_day = day
143
+ it.sources.append(source)
144
+ return it
145
+
146
+ def publish_public(world:World, fact_key:str, content:dict, day:int, base_cred:float, source_tag:str):
147
+ ki = KnowledgeItem(
148
+ id=str(uuid.uuid4())[:8], fact_key=fact_key, content=content,
149
+ first_seen_day=day, last_update_day=day, confidence=base_cred,
150
+ sources=[{"src":source_tag,"cred":base_cred}], is_public=True, visibility="public"
151
+ )
152
+ world.public_board.append(ki); return ki
153
+
154
+ def daily_decay(world:World, decay=0.98):
155
+ for a in world.agents:
156
+ for it in a.memory.values():
157
+ age = world.day - it.last_update_day
158
+ if age>0:
159
+ it.confidence = max(0.01, it.confidence*(decay**age))
160
+ for it in world.public_board:
161
+ age = world.day - it.last_update_day
162
+ if age>0:
163
+ it.confidence = max(0.01, it.confidence*(decay**age))
164
+
165
+ def inform(world:World, speaker:str, listener:str, fact_key:str, boost=0.15):
166
+ s = next(a for a in world.agents if a.name==speaker)
167
+ l = next(a for a in world.agents if a.name==listener)
168
+ it = s.memory.get(fact_key)
169
+ if not it: return "speaker knows nothing."
170
+ rel = world.relations[speaker][listener]
171
+ cred = min(0.95, max(0.2, it.confidence + boost + rel/200))
172
+ upsert_memory(l, fact_key, it.content, world.day, {"src":f"told_by_{speaker}","cred":cred}, cred)
173
+ # ToM: 我认为你知道了
174
+ s.belief_about_others.setdefault(listener, {})[fact_key] = {
175
+ "id": str(uuid.uuid4())[:8], "conf":cred, "content":it.content, "last_update_day": world.day
176
+ }
177
+ return "ok"
178
+
179
+ # ---------- 5) Actions(合欢宗简化版) ----------
180
+ def act_gift(world:World, giver:str, receiver:str, place="藏书阁"):
181
+ ev = make_event(world.day, "gift", [giver, receiver], {"place":place,"item":"清心茶"})
182
+ world.events[ev.id] = ev
183
+ # 当事人记忆
184
+ for nm in [giver, receiver]:
185
+ ag = next(a for a in world.agents if a.name==nm)
186
+ fk = f"gift_{giver}_{receiver}_d{world.day}"
187
+ upsert_memory(ag, fk, {"type":"gift","giver":giver,"receiver":receiver,"place":place},
188
+ world.day, {"src":"self_participant","cred":0.9}, 0.9)
189
+ # 旁观者
190
+ obs = sample_observers(world, ev, base_p=0.18)
191
+ world.observations += obs
192
+ for o in obs:
193
+ ag = next(a for a in world.agents if a.name==o.observer)
194
+ fk = f"gift_{giver}_{receiver}_d{world.day}"
195
+ conf = max(0.2, o.cred*(1-o.noise))
196
+ upsert_memory(ag, fk, {"type":"gift_hint","giver":giver,"receiver":receiver,"place":place},
197
+ world.day, {"src":o.mode,"cred":conf}, conf)
198
+ # 流言上板
199
+ if random.random() < 0.25:
200
+ publish_public(world, f"gift_{giver}_{receiver}",
201
+ {"hint":"有人在藏书阁递了东西"}, world.day, 0.55, "rumor")
202
+
203
+ def act_confide(world:World, a:str, b:str):
204
+ ev = make_event(world.day, "confide", [a,b], {"topic":"心法共鸣"})
205
+ world.events[ev.id] = ev
206
+ for nm in [a,b]:
207
+ ag = next(x for x in world.agents if x.name==nm)
208
+ fk = f"confide_{a}_{b}_d{world.day}"
209
+ upsert_memory(ag, fk, {"type":"confide","pair":[a,b]}, world.day, {"src":"private_talk","cred":0.95}, 0.95)
210
+ for o in sample_observers(world, ev, 0.12):
211
+ ag = next(x for x in world.agents if x.name==o.observer)
212
+ fk = f"confide_{a}_{b}_d{world.day}"
213
+ conf = max(0.15, o.cred*(1-o.noise))
214
+ upsert_memory(ag, fk, {"type":"confide_hint","pair":[a,b]}, world.day, {"src":o.mode,"cred":conf}, conf)
215
+
216
+ def act_expose(world:World, accuser:str, target:str, evidence:str):
217
+ ev = make_event(world.day, "expose", [accuser,target], {"evidence":evidence})
218
+ world.events[ev.id] = ev
219
+ fk = f"expose_{target}_d{world.day}"
220
+ pub = publish_public(world, fk, {"type":"expose","target":target,"evidence":evidence}, world.day, 0.8, f"expose_by_{accuser}")
221
+ for ag in world.agents:
222
+ upsert_memory(ag, fk, pub.content, world.day, {"src":"public_board","cred":pub.confidence}, pub.confidence)
223
+
224
+ # 其他动作:rumor/oath/sabotage/mediate/spar ...(按需补全)
225
+
226
+ # ---------- 6) Policy ----------
227
+ def agent_policy(world:World, actor:Agent):
228
+ # 简化:按角色偏好
229
+ names = [a.name for a in world.agents if a.name != actor.name]
230
+ tgt = random.choice(names)
231
+ if actor.role in ["Matchmaker"]: return ("oath", tgt)
232
+ if actor.role in ["Bard"]: return ("rumor", tgt)
233
+ if actor.role in ["Shadow"]: return ("expose", tgt)
234
+ if actor.role in ["Saintess"]: return ("mediate", tgt)
235
+ return random.choice([("gift", tgt), ("confide", tgt), ("spar", tgt)])
236
+
237
+ def apply_action(world:World, actor:str, action:str, target:str):
238
+ if action=="gift": act_gift(world, actor, target); return f"{actor}→{target} 赠礼"
239
+ if action=="confide": act_confide(world, actor, target); return f"{actor}���{target} 推心置腹"
240
+ if action=="expose": act_expose(world, actor, target, "残页证据"); return f"{actor} 公揭 {target}"
241
+ # TODO: rumor/oath/sabotage/mediate/spar...
242
+ return f"{actor} 今日独自修行"
243
+
244
+ # ---------- 7) Tick (推进一天) ----------
245
+ def tick_one_day(world:World):
246
+ world.day += 1
247
+ daily_events = []
248
+ # 每人 1 动作
249
+ for ag in world.agents:
250
+ act, tgt = agent_policy(world, ag)
251
+ daily_events.append(apply_action(world, ag.name, act, tgt))
252
+ # 信息衰减
253
+ daily_decay(world, decay=0.98)
254
+ # Narration
255
+ snap = json.dumps(world.snapshot(), ensure_ascii=False)[:2000]
256
+ prompt = f"DAY={world.day}\nWORLD_SNAPSHOT={snap}\nKEY_EVENTS={daily_events}\n请写今日纪要(2-4句):1) 核心八卦;2) 关系走向;3) 明日伏笔(可选)。"
257
+ narration = narrate(prompt)
258
+ return narration
259
+
260
+ # ---------- 8) God Mode ----------
261
+ def god_action(world:World, action:str, target:str="", value:int=0):
262
+ if action=="peach_blossom_trial" and target:
263
+ # 让所有人对 target 好感上升(用 relations 近似)
264
+ for ag in world.agents:
265
+ if ag.name!=target:
266
+ world.relations[ag.name][target] = min(100, world.relations[ag.name][target] + random.randint(2,6))
267
+ world.rumor_level += 10
268
+ return f"桃花劫降临 {target}"
269
+ if action=="inner_demon" and target:
270
+ ag = next(a for a in world.agents if a.name==target)
271
+ diff = random.randint(0,100) - (ag.willpower + value)
272
+ if diff>0:
273
+ ag.jealousy = min(100, ag.jealousy+12)
274
+ ag.scandal = min(100, ag.scandal+8)
275
+ return f"{target} 心魔试炼失败"
276
+ return f"{target} 心神稳固"
277
+ if action=="seclusion" and target:
278
+ # 简化:只记一条 public 提示
279
+ publish_public(world, f"seclusion_{target}", {"type":"seclusion","target":target,"days":value or 3}, world.day, 0.7, "decree")
280
+ return f"{target} 闭关 {value or 3} 日"
281
+ if action=="grand_banquet":
282
+ world.season="banquet"; world.rumor_level+=15
283
+ return "宗门盛会开启"
284
+ if action=="rumor_storm":
285
+ for ag in world.agents: ag.scandal = min(100, ag.scandal+5)
286
+ world.rumor_level += 12; return "流言四起,众心不安"
287
+ if action=="grant_fate" and target:
288
+ # 造一条“赐缘”公共事实(示意)
289
+ publish_public(world, f"grant_{target}", {"type":"grant_fate","target":target}, world.day, 0.75, "heaven")
290
+ return f"天机点化 {target}"
291
+ return "unknown god action"
292
+
293
+ # ---------- 10) Gradio UI ----------
294
+ def ui_init(seed):
295
+ global WORLD; WORLD = init_world(int(seed) if seed else 42)
296
+ return f"Reset ok. Day={WORLD.day}", json.dumps(WORLD.snapshot(), ensure_ascii=False, indent=2)
297
+
298
+ def ui_next_day():
299
+ nar = tick_one_day(WORLD)
300
+ return nar, json.dumps(WORLD.snapshot(), ensure_ascii=False, indent=2)
301
+
302
+ def ui_auto(days:int):
303
+ days = max(1, min(30, int(days)))
304
+ logs = []
305
+ for _ in range(days): logs.append(tick_one_day(WORLD))
306
+ return "\n\n".join(logs), json.dumps(WORLD.snapshot(), ensure_ascii=False, indent=2)
307
+
308
+ def ui_god(act, target, value):
309
+ msg = god_action(WORLD, act, target.strip(), int(value) if value else 0)
310
+ return msg, json.dumps(WORLD.snapshot(), ensure_ascii=False, indent=2)
311
+
312
+ def ui_public():
313
+ data = [asdict(x) for x in WORLD.public_board[-20:]]
314
+ return json.dumps(data, ensure_ascii=False, indent=2)
315
+
316
+ def ui_memory(agent_name):
317
+ ag = next((a for a in WORLD.agents if a.name==agent_name), None)
318
+ if not ag: return "no such agent"
319
+ data = {k:asdict(v) for k,v in ag.memory.items()}
320
+ return json.dumps(data, ensure_ascii=False, indent=2)
321
+
322
+ def ui_inform(speaker, listener, fact_key):
323
+ return inform(WORLD, speaker.strip(), listener.strip(), fact_key.strip())
324
+
325
+ with gr.Blocks() as demo:
326
+ gr.Markdown("## 合欢宗 · 多角恋八卦模拟(10 Agents, Day-based)")
327
+
328
+ with gr.Row():
329
+ seed = gr.Number(value=42, label="Seed")
330
+ btn_reset = gr.Button("Reset World")
331
+ init_out = gr.Textbox(label="Init")
332
+ snap_box = gr.Code(label="World Snapshot")
333
+ btn_reset.click(fn=ui_init, inputs=seed, outputs=[init_out, snap_box])
334
+
335
+ with gr.Row():
336
+ btn_next = gr.Button("Next Day")
337
+ auto_days = gr.Number(value=3, label="AFK days (1-30)")
338
+ btn_auto = gr.Button("Auto-Run")
339
+ narr = gr.Textbox(label="Narration", lines=6)
340
+ btn_next.click(fn=ui_next_day, inputs=None, outputs=[narr, snap_box])
341
+ btn_auto.click(fn=ui_auto, inputs=auto_days, outputs=[narr, snap_box])
342
+
343
+ gr.Markdown("### God Mode")
344
+ act = gr.Dropdown(["peach_blossom_trial","inner_demon","seclusion","grand_banquet","rumor_storm","grant_fate"], value="peach_blossom_trial")
345
+ tgt = gr.Textbox(value="A1", label="Target (可空)")
346
+ val = gr.Number(value=0, label="Value")
347
+ btn_god = gr.Button("Cast")
348
+ god_out = gr.Textbox(label="God Result")
349
+ btn_god.click(fn=ui_god, inputs=[act, tgt, val], outputs=[god_out, snap_box])
350
+
351
+ gr.Markdown("### Info System")
352
+ btn_pub = gr.Button("View Public Board")
353
+ pub_box = gr.Code(label="Public Board (recent)")
354
+ btn_pub.click(fn=ui_public, inputs=None, outputs=pub_box)
355
+
356
+ mem_agent = gr.Dropdown([f"A{i+1}" for i in range(10)], value="A1", label="Agent")
357
+ btn_mem = gr.Button("View Agent Memory")
358
+ mem_box = gr.Code(label="Private Memory (Agent)")
359
+ btn_mem.click(fn=ui_memory, inputs=mem_agent, outputs=mem_box)
360
+
361
+ gr.Markdown("### Inform (tell someone a fact_key)")
362
+ spk = gr.Textbox(value="A1", label="Speaker")
363
+ lst = gr.Textbox(value="A2", label="Listener")
364
+ fk = gr.Textbox(value="gift_A1_A2_d1", label="fact_key")
365
+ btn_inf = gr.Button("Inform")
366
+ inf_res = gr.Textbox(label="Inform Result")
367
+ btn_inf.click(fn=ui_inform, inputs=[spk,lst,fk], outputs=inf_res)
368
+
369
+ if __name__ == "__main__":
370
+ demo.launch()