Spaces:
Runtime error
Runtime error
Create app.py
Browse files
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()
|