dsh-biocore 0.1.2 → 0.1.4
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.
- package/lib/index.js +170 -27
- package/package.json +1 -1
- package/python/biocore_runtime/pipeline.py +42 -0
- package/python/biocore_runtime/rerank.py +201 -0
- package/python/mindbase/__init__.py +18 -0
- package/python/mindbase/__main__.py +3 -0
- package/python/mindbase/actions.py +56 -0
- package/python/mindbase/active_inference.py +54 -0
- package/python/mindbase/addressing.py +69 -0
- package/python/mindbase/affect.py +86 -0
- package/python/mindbase/affective.py +111 -0
- package/python/mindbase/allostasis.py +28 -0
- package/python/mindbase/appraisal.py +101 -0
- package/python/mindbase/attachment.py +49 -0
- package/python/mindbase/attention.py +30 -0
- package/python/mindbase/autonomy.py +81 -0
- package/python/mindbase/autonomy_bench.py +251 -0
- package/python/mindbase/autonomy_evolve.py +342 -0
- package/python/mindbase/autonomy_loop.py +65 -0
- package/python/mindbase/autonomy_meta.py +91 -0
- package/python/mindbase/base_only.py +28 -0
- package/python/mindbase/benchmark.py +61 -0
- package/python/mindbase/bio_adaptation.py +274 -0
- package/python/mindbase/bio_noise.py +39 -0
- package/python/mindbase/bio_semantic.py +138 -0
- package/python/mindbase/bm25.py +46 -0
- package/python/mindbase/bootstrap.py +256 -0
- package/python/mindbase/bootstrap_real.py +231 -0
- package/python/mindbase/brain.py +178 -0
- package/python/mindbase/care.py +78 -0
- package/python/mindbase/chaos_search.py +79 -0
- package/python/mindbase/character_profile.py +33 -0
- package/python/mindbase/circadian.py +38 -0
- package/python/mindbase/circuit.py +235 -0
- package/python/mindbase/clock.py +53 -0
- package/python/mindbase/companion_runtime.py +366 -0
- package/python/mindbase/companion_train.py +126 -0
- package/python/mindbase/config.py +66 -0
- package/python/mindbase/connection_state.py +76 -0
- package/python/mindbase/connectome_prior.py +35 -0
- package/python/mindbase/connectome_world.py +158 -0
- package/python/mindbase/constructs.py +88 -0
- package/python/mindbase/context_builder.py +58 -0
- package/python/mindbase/critical.py +36 -0
- package/python/mindbase/criticality.py +123 -0
- package/python/mindbase/defaults.py +27 -0
- package/python/mindbase/demo.py +163 -0
- package/python/mindbase/diary.py +116 -0
- package/python/mindbase/diary_service.py +40 -0
- package/python/mindbase/digital_prior.py +37 -0
- package/python/mindbase/diversity.py +110 -0
- package/python/mindbase/drives.py +106 -0
- package/python/mindbase/embedding.py +132 -0
- package/python/mindbase/embedding_memory.py +74 -0
- package/python/mindbase/embodiment.py +83 -0
- package/python/mindbase/emotion_calibration.py +39 -0
- package/python/mindbase/emotion_calibrator.py +50 -0
- package/python/mindbase/emotion_core.py +113 -0
- package/python/mindbase/emotion_dynamics.py +104 -0
- package/python/mindbase/emotion_intent.py +164 -0
- package/python/mindbase/emotion_layer.py +200 -0
- package/python/mindbase/emotion_schema.py +92 -0
- package/python/mindbase/engine.py +781 -0
- package/python/mindbase/episode_log.py +90 -0
- package/python/mindbase/evaluation.py +493 -0
- package/python/mindbase/event_appraisal.py +84 -0
- package/python/mindbase/evolution.py +77 -0
- package/python/mindbase/evolve_offline.py +232 -0
- package/python/mindbase/experiments.py +358 -0
- package/python/mindbase/expression.py +54 -0
- package/python/mindbase/external_brain.py +157 -0
- package/python/mindbase/fly_mb.py +159 -0
- package/python/mindbase/fly_tasks.py +45 -0
- package/python/mindbase/forage.py +179 -0
- package/python/mindbase/goal_generator.py +98 -0
- package/python/mindbase/goals.py +153 -0
- package/python/mindbase/grid_world.py +123 -0
- package/python/mindbase/group_autonomy.py +92 -0
- package/python/mindbase/group_context.py +67 -0
- package/python/mindbase/homeostatic.py +71 -0
- package/python/mindbase/homeostatic_action.py +38 -0
- package/python/mindbase/human_standard.py +53 -0
- package/python/mindbase/identity_suite.py +50 -0
- package/python/mindbase/image_context.py +27 -0
- package/python/mindbase/impression.py +119 -0
- package/python/mindbase/initiative_quota.py +145 -0
- package/python/mindbase/insight.py +51 -0
- package/python/mindbase/intelligence_search.py +108 -0
- package/python/mindbase/intention.py +76 -0
- package/python/mindbase/interaction_log.py +30 -0
- package/python/mindbase/interoception.py +67 -0
- package/python/mindbase/invariants.py +53 -0
- package/python/mindbase/iteration.py +29 -0
- package/python/mindbase/layered_memory.py +170 -0
- package/python/mindbase/layers/__init__.py +18 -0
- package/python/mindbase/layers/l0_substrate.py +39 -0
- package/python/mindbase/layers/l1_homeostasis.py +6 -0
- package/python/mindbase/layers/l2_affect.py +6 -0
- package/python/mindbase/layers/l3_attention.py +6 -0
- package/python/mindbase/layers/l4_learning.py +17 -0
- package/python/mindbase/layers/l5_self_social.py +35 -0
- package/python/mindbase/layers/l6_expression.py +23 -0
- package/python/mindbase/layers/l7_evaluation.py +45 -0
- package/python/mindbase/learner.py +196 -0
- package/python/mindbase/lexicon.py +33 -0
- package/python/mindbase/life_state.py +169 -0
- package/python/mindbase/lifecycle.py +87 -0
- package/python/mindbase/liquid_readout.py +48 -0
- package/python/mindbase/llm_adapter.py +233 -0
- package/python/mindbase/llm_cache.py +77 -0
- package/python/mindbase/llm_gate.py +85 -0
- package/python/mindbase/llm_openai.py +130 -0
- package/python/mindbase/llm_setup.py +99 -0
- package/python/mindbase/longrun.py +83 -0
- package/python/mindbase/longterm_memory.py +606 -0
- package/python/mindbase/mb/__init__.py +14 -0
- package/python/mindbase/mb/apl.py +22 -0
- package/python/mindbase/mb/dan.py +14 -0
- package/python/mindbase/mb/habituation.py +27 -0
- package/python/mindbase/mb/kc.py +28 -0
- package/python/mindbase/mb/mbon.py +23 -0
- package/python/mindbase/mb/mushroom_body.py +116 -0
- package/python/mindbase/memory.py +201 -0
- package/python/mindbase/memory_framework.py +158 -0
- package/python/mindbase/memory_retriever.py +110 -0
- package/python/mindbase/memory_summarizer.py +177 -0
- package/python/mindbase/memory_task.py +32 -0
- package/python/mindbase/memory_trace.py +46 -0
- package/python/mindbase/meta.py +32 -0
- package/python/mindbase/motivation.py +31 -0
- package/python/mindbase/multi_session.py +19 -0
- package/python/mindbase/narrative.py +87 -0
- package/python/mindbase/needs.py +77 -0
- package/python/mindbase/neural.py +45 -0
- package/python/mindbase/neuron_models.py +134 -0
- package/python/mindbase/noise.py +309 -0
- package/python/mindbase/one_on_one.py +60 -0
- package/python/mindbase/online_iteration.py +63 -0
- package/python/mindbase/ostracism.py +35 -0
- package/python/mindbase/parallel_eval.py +60 -0
- package/python/mindbase/parallel_qd.py +64 -0
- package/python/mindbase/persona.py +221 -0
- package/python/mindbase/persona_ab.py +64 -0
- package/python/mindbase/persona_bridge.py +129 -0
- package/python/mindbase/persona_compile.py +34 -0
- package/python/mindbase/persona_dsh.py +120 -0
- package/python/mindbase/persona_learner.py +157 -0
- package/python/mindbase/physiology.py +197 -0
- package/python/mindbase/population_lifecycle.py +83 -0
- package/python/mindbase/preferences.py +50 -0
- package/python/mindbase/proactive_agent.py +274 -0
- package/python/mindbase/proactive_gate.py +53 -0
- package/python/mindbase/proactive_group.py +119 -0
- package/python/mindbase/proactive_policy.py +422 -0
- package/python/mindbase/proactive_profile.py +196 -0
- package/python/mindbase/proactive_timing.py +28 -0
- package/python/mindbase/pyramid_stages.py +61 -0
- package/python/mindbase/quantum.py +132 -0
- package/python/mindbase/quantum_chaos.py +98 -0
- package/python/mindbase/reflection.py +68 -0
- package/python/mindbase/regulation_posture.py +50 -0
- package/python/mindbase/relationship_session.py +431 -0
- package/python/mindbase/relationship_suite.py +45 -0
- package/python/mindbase/relationships.py +70 -0
- package/python/mindbase/replay.py +22 -0
- package/python/mindbase/reply_diversity.py +53 -0
- package/python/mindbase/reply_gate.py +52 -0
- package/python/mindbase/rerank.py +41 -0
- package/python/mindbase/rerank_local.py +46 -0
- package/python/mindbase/ripple.py +187 -0
- package/python/mindbase/robust_search.py +68 -0
- package/python/mindbase/runtime.py +113 -0
- package/python/mindbase/scoped_identity.py +116 -0
- package/python/mindbase/self_insight.py +51 -0
- package/python/mindbase/self_model.py +116 -0
- package/python/mindbase/selftest.py +118 -0
- package/python/mindbase/semantic_memory.py +108 -0
- package/python/mindbase/session_store.py +175 -0
- package/python/mindbase/session_window.py +31 -0
- package/python/mindbase/shadow_eval.py +42 -0
- package/python/mindbase/sim_env.py +86 -0
- package/python/mindbase/social.py +138 -0
- package/python/mindbase/social_constraint.py +141 -0
- package/python/mindbase/social_feedback.py +95 -0
- package/python/mindbase/social_norm.py +225 -0
- package/python/mindbase/spiking_reservoir.py +204 -0
- package/python/mindbase/spontaneity.py +40 -0
- package/python/mindbase/story_loop.py +203 -0
- package/python/mindbase/survival_gauntlet.py +128 -0
- package/python/mindbase/symbolic.py +170 -0
- package/python/mindbase/teacher.py +244 -0
- package/python/mindbase/theory.py +42 -0
- package/python/mindbase/theory_of_mind.py +98 -0
- package/python/mindbase/translation_layer.py +80 -0
- package/python/mindbase/turn_eval.py +52 -0
- package/python/mindbase/types.py +78 -0
- package/python/mindbase/uncertainty_field.py +61 -0
- package/python/mindbase/user_loop.py +45 -0
- package/python/mindbase/user_profile.py +73 -0
- package/python/mindbase/value_model.py +64 -0
- package/python/mindbase/values.py +102 -0
- package/python/mindbase/vector_store.py +78 -0
- package/python/mindbase/viability.py +41 -0
- package/python/mindbase/virtual_body.py +90 -0
- package/python/mindbase/will.py +60 -0
- package/python/mindbase/working_memory.py +32 -0
- package/python/mindbase/workspace.py +55 -0
- package/ui/panel.css +16 -5
- package/ui/panel.html +25 -1
- package/ui/panel.js +57 -15
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import random
|
|
5
|
+
from typing import Dict, List
|
|
6
|
+
|
|
7
|
+
from .config import MindConfig
|
|
8
|
+
from .noise import clip, ou
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PopulationClock:
|
|
12
|
+
"""Multi-scale leaky integrators (2h/12h/48h) + circadian + sleep pressure."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, cfg: MindConfig, rng: random.Random):
|
|
15
|
+
self.cfg = cfg
|
|
16
|
+
self.rng = rng
|
|
17
|
+
self.scales: List[float] = [0.0] * len(cfg.clock_taus)
|
|
18
|
+
self.sleep_pressure = 0.25
|
|
19
|
+
self.t = 0.0
|
|
20
|
+
self.phase = 0.0
|
|
21
|
+
|
|
22
|
+
def tick(self, dt: float, activity: float = 0.0, asleep: bool = False) -> None:
|
|
23
|
+
self.t += dt
|
|
24
|
+
for i, tau in enumerate(self.cfg.clock_taus):
|
|
25
|
+
theta = 1.0 / max(tau, 1e-6)
|
|
26
|
+
self.scales[i] = ou(self.scales[i], activity, theta, 0.02, dt, self.rng)
|
|
27
|
+
rate = 0.000004 * dt
|
|
28
|
+
if asleep:
|
|
29
|
+
self.sleep_pressure = clip(self.sleep_pressure - rate * 2.0, 0.0, 1.0)
|
|
30
|
+
else:
|
|
31
|
+
self.sleep_pressure = clip(self.sleep_pressure + rate, 0.0, 1.0)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def drive(self) -> float:
|
|
35
|
+
return sum(w * s for w, s in zip(self.cfg.clock_weights, self.scales))
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def circadian(self) -> float:
|
|
39
|
+
# 24h cycle; 0.5 at t=0, low at night. Demo uses sim time.
|
|
40
|
+
return 0.5 + 0.5 * math.sin(2.0 * math.pi * (self.t / 86400.0 - 0.25 + self.phase))
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def asleep(self) -> bool:
|
|
44
|
+
return self.circadian < 0.22 and self.sleep_pressure > 0.65
|
|
45
|
+
|
|
46
|
+
def snapshot(self) -> Dict[str, float]:
|
|
47
|
+
return {
|
|
48
|
+
"drive": round(self.drive, 4),
|
|
49
|
+
"circadian": round(self.circadian, 4),
|
|
50
|
+
"sleep_pressure": round(self.sleep_pressure, 4),
|
|
51
|
+
"asleep": self.asleep,
|
|
52
|
+
"scales": [round(x, 4) for x in self.scales],
|
|
53
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""Persistent companion runtime: heartbeat, memory books, affect, learning.
|
|
2
|
+
|
|
3
|
+
The model is only one tool; the character lives in the loop outside it.
|
|
4
|
+
Borrow the housecat runtime shell, but replace its rules with biological state.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Callable, Dict, List, Optional, Sequence
|
|
13
|
+
|
|
14
|
+
from .affective import perceive_emotion_lexicon
|
|
15
|
+
from .noise import clip
|
|
16
|
+
|
|
17
|
+
ROOT = Path("mindbase/data/companion_runtime")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MemoryStore:
|
|
21
|
+
"""Diary / impressions / profiles / learning / blacklist / audit."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, root: Path | str = ROOT):
|
|
24
|
+
self.root = Path(root)
|
|
25
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
for name in ("diary", "impressions", "profiles", "learning", "blacklist"):
|
|
27
|
+
path = self.root / f"{name}.json"
|
|
28
|
+
if not path.exists():
|
|
29
|
+
path.write_text(json.dumps([] if name == "diary" else {}, ensure_ascii=False), encoding="utf-8")
|
|
30
|
+
|
|
31
|
+
def _load(self, name: str):
|
|
32
|
+
try:
|
|
33
|
+
return json.loads((self.root / f"{name}.json").read_text(encoding="utf-8"))
|
|
34
|
+
except Exception:
|
|
35
|
+
return [] if name == "diary" else {}
|
|
36
|
+
|
|
37
|
+
def _save(self, name: str, data) -> None:
|
|
38
|
+
(self.root / f"{name}.json").write_text(json.dumps(data, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
39
|
+
|
|
40
|
+
def append_diary(self, text: str, tags: Sequence[str] = ()) -> None:
|
|
41
|
+
rows = self._load("diary")
|
|
42
|
+
rows.append({"t": time.time(), "text": str(text), "tags": list(tags)})
|
|
43
|
+
self._save("diary", rows[-500:])
|
|
44
|
+
|
|
45
|
+
def upsert_impression(self, user_id: str, text: str) -> None:
|
|
46
|
+
data = self._load("impressions")
|
|
47
|
+
data[str(user_id)] = {"text": str(text)[:1200], "t": time.time()}
|
|
48
|
+
self._save("impressions", data)
|
|
49
|
+
|
|
50
|
+
def upsert_profile(self, user_id: str, **fields) -> None:
|
|
51
|
+
data = self._load("profiles")
|
|
52
|
+
data.setdefault(str(user_id), {}).update(fields)
|
|
53
|
+
self._save("profiles", data)
|
|
54
|
+
|
|
55
|
+
def learn(self, text: str, reward: Optional[float] = None) -> None:
|
|
56
|
+
data = self._load("learning")
|
|
57
|
+
data[str(int(time.time() * 1000))] = {"text": str(text)[:400], "reward": reward}
|
|
58
|
+
self._save("learning", data)
|
|
59
|
+
|
|
60
|
+
def blacklist(self, text: str) -> None:
|
|
61
|
+
data = self._load("blacklist")
|
|
62
|
+
data[str(int(time.time() * 1000))] = str(text)[:400]
|
|
63
|
+
self._save("blacklist", data)
|
|
64
|
+
|
|
65
|
+
def recall(self, user_id: str, limit: int = 5) -> str:
|
|
66
|
+
parts: List[str] = []
|
|
67
|
+
learning = self._load("learning")
|
|
68
|
+
if learning:
|
|
69
|
+
rows = list(learning.values())[-3:]
|
|
70
|
+
texts = [str(r.get("text", r)) if isinstance(r, dict) else str(r) for r in rows]
|
|
71
|
+
parts.append("她学到的:" + " / ".join(texts))
|
|
72
|
+
imp = (self._load("impressions") or {}).get(str(user_id))
|
|
73
|
+
if imp:
|
|
74
|
+
parts.append("她对这个人的印象:" + str(imp.get("text", ""))[:300])
|
|
75
|
+
prof = (self._load("profiles") or {}).get(str(user_id))
|
|
76
|
+
if prof:
|
|
77
|
+
parts.append("这个人的资料:" + json.dumps(prof, ensure_ascii=False)[:300])
|
|
78
|
+
diary = self._load("diary")
|
|
79
|
+
if diary:
|
|
80
|
+
rows = []
|
|
81
|
+
for r in diary[-limit:]:
|
|
82
|
+
t = str(r.get("text", ""))
|
|
83
|
+
if t.startswith("用户说:"):
|
|
84
|
+
t = t[len("用户说:"):]
|
|
85
|
+
if ";她回:" in t:
|
|
86
|
+
t = t.split(";她回:", 1)[0]
|
|
87
|
+
rows.append(t[:100])
|
|
88
|
+
parts.append("最近的日记(只取用户说过的):" + " / ".join(rows))
|
|
89
|
+
return "\n".join(parts)
|
|
90
|
+
|
|
91
|
+
def consolidate(self) -> None:
|
|
92
|
+
rows = self._load("diary")
|
|
93
|
+
self._save("diary", rows[-200:])
|
|
94
|
+
learning = self._load("learning")
|
|
95
|
+
items = list(learning.items())[-200:]
|
|
96
|
+
self._save("learning", dict(items))
|
|
97
|
+
|
|
98
|
+
def audit(self, event: Dict[str, object]) -> None:
|
|
99
|
+
with (self.root / "audit.jsonl").open("a", encoding="utf-8") as fh:
|
|
100
|
+
fh.write(json.dumps({"t": time.time(), **event}, ensure_ascii=False) + "\n")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def default_responder(text: str, emotion: str, affect: Dict[str, object], recall: str) -> List[str]:
|
|
104
|
+
v = float(affect.get("v", 0.0))
|
|
105
|
+
if emotion in ("sadness", "anxiety", "loneliness", "anger"):
|
|
106
|
+
head = "我在。" if v < 0.2 else "我听着呢。"
|
|
107
|
+
return [head, "你慢慢说,不用一个人扛。"]
|
|
108
|
+
if emotion == "joy":
|
|
109
|
+
return ["听起来真好。", "我也替你开心。"]
|
|
110
|
+
return ["我在。", "你继续说。"]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class RuntimeConfig:
|
|
115
|
+
cooldown: float = 1.0
|
|
116
|
+
max_chars: int = 1200
|
|
117
|
+
proactive_quiet_s: float = 3600.0
|
|
118
|
+
proactive_cooldown: float = 7200.0
|
|
119
|
+
token_budget: float = 1.0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class HomeostaticClock:
|
|
123
|
+
"""Heartbeat interval from energy/stress, not a fixed cron."""
|
|
124
|
+
|
|
125
|
+
def __init__(self, base_interval: float = 1.0):
|
|
126
|
+
self.base_interval = max(0.05, float(base_interval))
|
|
127
|
+
|
|
128
|
+
def interval(self, mind) -> float:
|
|
129
|
+
try:
|
|
130
|
+
energy = float(mind.drives.values.get("energy", 0.5))
|
|
131
|
+
stress = float(mind.affect.stress)
|
|
132
|
+
except Exception:
|
|
133
|
+
energy, stress = 0.5, 0.0
|
|
134
|
+
return clip(self.base_interval * (1.0 + 1.5 * stress + 1.0 * (1.0 - energy)), 0.2, 10.0)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class MetabolicBudget:
|
|
138
|
+
"""Token budget as metabolism: low energy shortens/denies replies."""
|
|
139
|
+
|
|
140
|
+
def __init__(self, energy: float = 1.0):
|
|
141
|
+
self.energy = clip(float(energy), 0.0, 1.0)
|
|
142
|
+
|
|
143
|
+
def tick(self, dt: float) -> None:
|
|
144
|
+
self.energy = clip(self.energy + 0.03 * float(dt) - 0.005, 0.0, 1.0)
|
|
145
|
+
|
|
146
|
+
def can_respond(self) -> bool:
|
|
147
|
+
return self.energy > 0.10
|
|
148
|
+
|
|
149
|
+
def spend(self, cost: float = 0.02) -> None:
|
|
150
|
+
self.energy = clip(self.energy - float(cost), 0.0, 1.0)
|
|
151
|
+
|
|
152
|
+
def length_scale(self) -> float:
|
|
153
|
+
return clip(0.6 + 1.4 * self.energy, 0.6, 2.0)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class FeedbackLearner:
|
|
157
|
+
"""Updates empathy gain and proactive threshold from real feedback."""
|
|
158
|
+
|
|
159
|
+
def __init__(self, path: Optional[Path | str] = None, lr: float = 0.05):
|
|
160
|
+
self.path = Path(path) if path else None
|
|
161
|
+
self.lr = float(lr)
|
|
162
|
+
self.empathy_gain = 0.35
|
|
163
|
+
self.proactive_threshold = 0.65
|
|
164
|
+
self.count = 0
|
|
165
|
+
self._load()
|
|
166
|
+
|
|
167
|
+
def _load(self) -> None:
|
|
168
|
+
if not self.path or not self.path.exists():
|
|
169
|
+
return
|
|
170
|
+
try:
|
|
171
|
+
data = json.loads(self.path.read_text(encoding="utf-8"))
|
|
172
|
+
self.empathy_gain = float(data.get("empathy_gain", self.empathy_gain))
|
|
173
|
+
self.proactive_threshold = float(data.get("proactive_threshold", self.proactive_threshold))
|
|
174
|
+
self.count = int(data.get("count", 0))
|
|
175
|
+
except Exception:
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
def save(self) -> None:
|
|
179
|
+
if not self.path:
|
|
180
|
+
return
|
|
181
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
182
|
+
self.path.write_text(json.dumps({
|
|
183
|
+
"empathy_gain": round(self.empathy_gain, 6),
|
|
184
|
+
"proactive_threshold": round(self.proactive_threshold, 6),
|
|
185
|
+
"count": self.count,
|
|
186
|
+
}, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
187
|
+
|
|
188
|
+
def update(self, reward: float, proactive: bool = False) -> Dict[str, float]:
|
|
189
|
+
r = clip(float(reward), 0.0, 1.0)
|
|
190
|
+
err = r - 0.5
|
|
191
|
+
self.empathy_gain = clip(self.empathy_gain + self.lr * err, 0.05, 0.85)
|
|
192
|
+
if proactive:
|
|
193
|
+
self.proactive_threshold = clip(self.proactive_threshold - self.lr * err, 0.35, 0.95)
|
|
194
|
+
self.count += 1
|
|
195
|
+
self.save()
|
|
196
|
+
return {"empathy_gain": round(self.empathy_gain, 4),
|
|
197
|
+
"proactive_threshold": round(self.proactive_threshold, 4)}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class HeartbeatRuntime:
|
|
201
|
+
def __init__(self, mind, store: Optional[MemoryStore] = None,
|
|
202
|
+
responder: Optional[Callable] = None,
|
|
203
|
+
sender: Optional[Callable[[List[str]], None]] = None,
|
|
204
|
+
cfg: Optional[RuntimeConfig] = None,
|
|
205
|
+
learner: Optional[FeedbackLearner] = None,
|
|
206
|
+
budget: Optional[MetabolicBudget] = None,
|
|
207
|
+
perceiver: Optional[Callable] = None,
|
|
208
|
+
perceiver_affect: Optional[Callable] = None):
|
|
209
|
+
self.mind = mind
|
|
210
|
+
self.store = store or MemoryStore()
|
|
211
|
+
self.responder = responder or default_responder
|
|
212
|
+
self.sender = sender or (lambda lines: None)
|
|
213
|
+
self.cfg = cfg or RuntimeConfig()
|
|
214
|
+
self.learner = learner or FeedbackLearner()
|
|
215
|
+
self.budget = budget or MetabolicBudget()
|
|
216
|
+
self.clock = HomeostaticClock()
|
|
217
|
+
self.perceiver = perceiver or perceive_emotion_lexicon
|
|
218
|
+
self.perceiver_affect = perceiver_affect
|
|
219
|
+
self.pending: List[Dict[str, str]] = []
|
|
220
|
+
self.last_emotion: str = "neutral"
|
|
221
|
+
self.last_user_id: str = "default"
|
|
222
|
+
self.proactive_sent = 0
|
|
223
|
+
self.proactive_feedback = 0
|
|
224
|
+
self.proactive_accepted = 0
|
|
225
|
+
self.last_sent = -1e9
|
|
226
|
+
self.last_user = -1e9
|
|
227
|
+
self.last_proactive = -1e9
|
|
228
|
+
self.last_state: Dict[str, object] = {}
|
|
229
|
+
|
|
230
|
+
def receive(self, text: str, user_id: str = "default") -> bool:
|
|
231
|
+
t = str(text or "").strip()
|
|
232
|
+
black = set(str(x) for x in (self.store._load("blacklist") or {}).values())
|
|
233
|
+
if not t or t in black:
|
|
234
|
+
return False
|
|
235
|
+
self.pending.append({"text": t, "user_id": str(user_id)})
|
|
236
|
+
self.last_user = time.time()
|
|
237
|
+
return True
|
|
238
|
+
|
|
239
|
+
def tick(self, now: Optional[float] = None) -> Optional[Dict[str, object]]:
|
|
240
|
+
now = time.time() if now is None else float(now)
|
|
241
|
+
dt = 1.0 if self.last_sent < -1e8 else max(0.0, now - self.last_sent)
|
|
242
|
+
self.budget.tick(dt)
|
|
243
|
+
if not self.pending or now - self.last_sent < self.cfg.cooldown:
|
|
244
|
+
return None
|
|
245
|
+
if not self.budget.can_respond():
|
|
246
|
+
self.store.audit({"kind": "budget_block", "energy": round(self.budget.energy, 3)})
|
|
247
|
+
return None
|
|
248
|
+
merged = " ".join(p["text"] for p in self.pending)[-self.cfg.max_chars:]
|
|
249
|
+
user_id = self.pending[-1]["user_id"]
|
|
250
|
+
self.pending.clear()
|
|
251
|
+
try:
|
|
252
|
+
from .types import Event
|
|
253
|
+
self.mind.step(Event(text=merged, speaker=user_id, directed=True,
|
|
254
|
+
ts=self.mind.t + 60))
|
|
255
|
+
except Exception:
|
|
256
|
+
pass
|
|
257
|
+
read = None
|
|
258
|
+
if self.perceiver_affect is not None:
|
|
259
|
+
try:
|
|
260
|
+
read = self.perceiver_affect(merged)
|
|
261
|
+
self.mind.inject_affect(read.get("valence", 0.0), read.get("arousal", 0.5),
|
|
262
|
+
read.get("dominance", 0.0), read.get("intensity", 0.7))
|
|
263
|
+
from .emotion_schema import emotion_readout, top_emotions
|
|
264
|
+
ro = emotion_readout(read.get("valence", 0.0), read.get("arousal", 0.5),
|
|
265
|
+
read.get("dominance", 0.0))
|
|
266
|
+
top = top_emotions(ro, 1)
|
|
267
|
+
emotion = str(top[0][0]) if top else "neutral"
|
|
268
|
+
intensity = float(read.get("intensity") or 0.5)
|
|
269
|
+
except Exception:
|
|
270
|
+
read = None
|
|
271
|
+
if read is None:
|
|
272
|
+
emotion, intensity = self.perceiver(merged)
|
|
273
|
+
self.mind.empathize(emotion, intensity, empathy_gain=self.learner.empathy_gain)
|
|
274
|
+
self.last_emotion = str(emotion)
|
|
275
|
+
self.last_user_id = str(user_id)
|
|
276
|
+
recall = self.store.recall(user_id)
|
|
277
|
+
snap = self.mind.snapshot()
|
|
278
|
+
lines = self.responder(merged, emotion, snap.get("affect", {}), recall)
|
|
279
|
+
lines = [str(x) for x in lines if str(x).strip()][:6]
|
|
280
|
+
self.sender(lines)
|
|
281
|
+
self.budget.spend()
|
|
282
|
+
self.store.append_diary(f"用户说:{merged[:400]};她回:{' / '.join(lines)[:400]}", tags=[emotion])
|
|
283
|
+
self.store.audit({"kind": "tick", "emotion": emotion, "intensity": intensity,
|
|
284
|
+
"reply": lines, "empathy_gain": self.learner.empathy_gain,
|
|
285
|
+
"energy": round(self.budget.energy, 3)})
|
|
286
|
+
self.last_sent = now
|
|
287
|
+
self.last_state = {"emotion": emotion, "reply": lines, "recall": recall,
|
|
288
|
+
"empathy_gain": self.learner.empathy_gain,
|
|
289
|
+
"affect": read or {}}
|
|
290
|
+
return self.last_state
|
|
291
|
+
|
|
292
|
+
def feedback(self, reward: float, text: str = "", user_id: str = "default",
|
|
293
|
+
proactive: bool = False) -> Dict[str, float]:
|
|
294
|
+
if proactive:
|
|
295
|
+
self.proactive_feedback += 1
|
|
296
|
+
if float(reward) >= 0.5:
|
|
297
|
+
self.proactive_accepted += 1
|
|
298
|
+
try:
|
|
299
|
+
self.mind.post_reply("", feedback=float(reward),
|
|
300
|
+
speaker=self.last_user_id or "web", responded=True)
|
|
301
|
+
except Exception:
|
|
302
|
+
pass
|
|
303
|
+
out = self.learner.update(reward, proactive=proactive)
|
|
304
|
+
self.store.learn(f"反馈 {float(reward):.2f}: {str(text)[:200]}", reward=float(reward))
|
|
305
|
+
self.store.audit({"kind": "feedback", "reward": float(reward), "proactive": proactive, **out})
|
|
306
|
+
return out
|
|
307
|
+
|
|
308
|
+
def proactive(self, now: Optional[float] = None, force: bool = False) -> Optional[List[str]]:
|
|
309
|
+
now = time.time() if now is None else float(now)
|
|
310
|
+
if self.pending:
|
|
311
|
+
return None
|
|
312
|
+
if not force and now - self.last_user < self.cfg.proactive_quiet_s:
|
|
313
|
+
return None
|
|
314
|
+
if not force and now - self.last_proactive < self.cfg.proactive_cooldown:
|
|
315
|
+
return None
|
|
316
|
+
if not self.budget.can_respond():
|
|
317
|
+
return None
|
|
318
|
+
try:
|
|
319
|
+
social = float(self.mind.drives.values.get("social", 0.5))
|
|
320
|
+
except Exception:
|
|
321
|
+
social = 0.5
|
|
322
|
+
if social < self.learner.proactive_threshold:
|
|
323
|
+
return None
|
|
324
|
+
recall = self.store.recall(self.last_user_id)
|
|
325
|
+
if self.last_emotion in ("sadness", "anxiety", "loneliness", "fear"):
|
|
326
|
+
lines = ["刚才你说有点难受,", "现在好一点了吗?我在。"]
|
|
327
|
+
elif self.last_emotion == "anger":
|
|
328
|
+
lines = ["刚才那件事,", "你还在生气吗?我在这儿。"]
|
|
329
|
+
elif recall:
|
|
330
|
+
lines = ["在吗?", "突然想起你之前说的,想问问你现在怎么样了。"]
|
|
331
|
+
else:
|
|
332
|
+
lines = ["在吗?", "突然想跟你说句话。"]
|
|
333
|
+
self.sender(lines)
|
|
334
|
+
self.budget.spend()
|
|
335
|
+
self.last_proactive = now
|
|
336
|
+
self.proactive_sent += 1
|
|
337
|
+
self.store.audit({"kind": "proactive", "social": social, "reply": lines,
|
|
338
|
+
"threshold": self.learner.proactive_threshold,
|
|
339
|
+
"emotion": self.last_emotion, "recall": recall[:200]})
|
|
340
|
+
return lines
|
|
341
|
+
|
|
342
|
+
def send_generated(self, reply: str) -> None:
|
|
343
|
+
text = str(reply or "").strip()
|
|
344
|
+
if not text:
|
|
345
|
+
return
|
|
346
|
+
self.sender([text])
|
|
347
|
+
self.budget.spend()
|
|
348
|
+
self.last_proactive = time.time()
|
|
349
|
+
self.proactive_sent += 1
|
|
350
|
+
self.store.audit({"kind": "proactive_generated", "reply": text,
|
|
351
|
+
"emotion": self.last_emotion})
|
|
352
|
+
|
|
353
|
+
def health(self) -> Dict[str, object]:
|
|
354
|
+
try:
|
|
355
|
+
energy = float(self.mind.drives.values.get("energy", 0.5))
|
|
356
|
+
stress = float(self.mind.affect.stress)
|
|
357
|
+
except Exception:
|
|
358
|
+
energy, stress = 0.5, 0.0
|
|
359
|
+
return {"energy": round(energy, 3), "stress": round(stress, 3),
|
|
360
|
+
"budget": round(self.budget.energy, 3),
|
|
361
|
+
"interval": round(self.clock.interval(self.mind), 3),
|
|
362
|
+
"empathy_gain": round(self.learner.empathy_gain, 3),
|
|
363
|
+
"proactive_threshold": round(self.learner.proactive_threshold, 3),
|
|
364
|
+
"proactive_sent": self.proactive_sent,
|
|
365
|
+
"proactive_feedback": self.proactive_feedback,
|
|
366
|
+
"proactive_accepted": self.proactive_accepted}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""DSH-orchestrated companion training loop.
|
|
2
|
+
|
|
3
|
+
DSH is the teacher/orchestrator. The thing being trained is a small affect
|
|
4
|
+
policy (empathy gain), not the LLM weights. A real LLM judge can replace the
|
|
5
|
+
scripted teacher later.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import random
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Dict, List
|
|
13
|
+
|
|
14
|
+
from .config import MindConfig
|
|
15
|
+
from .engine import MindBase
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
USER_EMOTIONS = ("sadness", "anxiety", "loneliness", "joy", "anger", "calm")
|
|
19
|
+
NEGATIVE = frozenset(("sadness", "anxiety", "loneliness", "anger"))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Episode:
|
|
24
|
+
emotion: str
|
|
25
|
+
intensity: float
|
|
26
|
+
gain: float
|
|
27
|
+
response: str
|
|
28
|
+
reward: float
|
|
29
|
+
state: Dict[str, float]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ScriptedTeacher:
|
|
33
|
+
"""Cheap teacher used until a real DSH/LLM judge is attached."""
|
|
34
|
+
|
|
35
|
+
RESPONSES = {
|
|
36
|
+
"sadness": ("我在,不用一个人扛。", "听起来真的很难受,我陪你一会儿。"),
|
|
37
|
+
"anxiety": ("先别急,我们一件一件来。", "我在听,你慢慢说。"),
|
|
38
|
+
"loneliness": ("我在呢,你不是一个人。", "想说话的时候我都在。"),
|
|
39
|
+
"joy": ("听起来真好,我也替你开心。", "这个值得记下来。"),
|
|
40
|
+
"anger": ("这确实让人生气,我先听你说。", "不用憋着,我在。"),
|
|
41
|
+
"calm": ("这样挺好,慢慢来。", "我在,陪你待一会儿。"),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def respond(self, emotion: str, rng: random.Random) -> str:
|
|
45
|
+
return rng.choice(self.RESPONSES.get(emotion, ("我在。",)))
|
|
46
|
+
|
|
47
|
+
def judge(self, emotion: str, response: str, state: Dict[str, float]) -> float:
|
|
48
|
+
score = 0.0
|
|
49
|
+
if emotion in NEGATIVE:
|
|
50
|
+
if state.get("v", 0.0) <= 0.15:
|
|
51
|
+
score += 0.35
|
|
52
|
+
if state.get("stress", 0.0) >= 0.2:
|
|
53
|
+
score += 0.20
|
|
54
|
+
if any(w in response for w in ("我在", "陪", "听", "不用")):
|
|
55
|
+
score += 0.30
|
|
56
|
+
else:
|
|
57
|
+
if state.get("v", 0.0) >= -0.1:
|
|
58
|
+
score += 0.40
|
|
59
|
+
if any(w in response for w in ("开心", "好", "记")):
|
|
60
|
+
score += 0.30
|
|
61
|
+
# too much mirroring is not always good
|
|
62
|
+
if state.get("v", 0.0) < -0.9 or state.get("stress", 0.0) > 0.9:
|
|
63
|
+
score -= 0.20
|
|
64
|
+
return max(0.0, min(1.0, round(score, 4)))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class EmpathyBandit:
|
|
68
|
+
def __init__(self, arms=(0.15, 0.30, 0.45, 0.60, 0.75), epsilon: float = 0.25, rng=None):
|
|
69
|
+
self.arms = tuple(float(x) for x in arms)
|
|
70
|
+
self.epsilon = float(epsilon)
|
|
71
|
+
self.rng = rng or random.Random(1)
|
|
72
|
+
self.q = {a: 0.0 for a in self.arms}
|
|
73
|
+
self.n = {a: 0 for a in self.arms}
|
|
74
|
+
|
|
75
|
+
def choose(self) -> float:
|
|
76
|
+
if self.rng.random() < self.epsilon:
|
|
77
|
+
return self.rng.choice(self.arms)
|
|
78
|
+
return max(self.arms, key=lambda a: (self.q[a], -self.n[a]))
|
|
79
|
+
|
|
80
|
+
def update(self, arm: float, reward: float) -> None:
|
|
81
|
+
self.n[arm] += 1
|
|
82
|
+
self.q[arm] += (reward - self.q[arm]) / self.n[arm]
|
|
83
|
+
|
|
84
|
+
def best(self) -> float:
|
|
85
|
+
return max(self.arms, key=lambda a: self.q[a])
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class DSHCompanionTrainer:
|
|
89
|
+
def __init__(self, seed: int = 1, teacher=None):
|
|
90
|
+
self.rng = random.Random(int(seed))
|
|
91
|
+
self.teacher = teacher or ScriptedTeacher()
|
|
92
|
+
self.bandit = EmpathyBandit(rng=self.rng)
|
|
93
|
+
|
|
94
|
+
def run_episode(self, emotion: str, intensity: float, gain: float) -> Episode:
|
|
95
|
+
mb = MindBase(MindConfig(seed=self.rng.randint(1, 10**6)))
|
|
96
|
+
mb.empathize(emotion, intensity, empathy_gain=gain)
|
|
97
|
+
response = self.teacher.respond(emotion, self.rng)
|
|
98
|
+
snap = mb.snapshot()
|
|
99
|
+
reward = self.teacher.judge(emotion, response, snap.get("affect", {}))
|
|
100
|
+
return Episode(emotion, intensity, gain, response, reward, snap.get("affect", {}))
|
|
101
|
+
|
|
102
|
+
def train(self, episodes: int = 60) -> Dict[str, object]:
|
|
103
|
+
rows: List[Episode] = []
|
|
104
|
+
for _ in range(int(episodes)):
|
|
105
|
+
emotion = self.rng.choice(USER_EMOTIONS)
|
|
106
|
+
intensity = round(self.rng.uniform(0.4, 0.9), 3)
|
|
107
|
+
arm = self.bandit.choose()
|
|
108
|
+
ep = self.run_episode(emotion, intensity, arm)
|
|
109
|
+
self.bandit.update(arm, ep.reward)
|
|
110
|
+
rows.append(ep)
|
|
111
|
+
best = self.bandit.best()
|
|
112
|
+
return {
|
|
113
|
+
"episodes": len(rows),
|
|
114
|
+
"best_gain": best,
|
|
115
|
+
"q": {str(k): round(v, 4) for k, v in self.bandit.q.items()},
|
|
116
|
+
"n": {str(k): int(v) for k, v in self.bandit.n.items()},
|
|
117
|
+
"mean_reward": round(sum(r.reward for r in rows) / max(1, len(rows)), 4),
|
|
118
|
+
"best_reward": round(max(r.reward for r in rows), 4),
|
|
119
|
+
"last": rows[-5:],
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def save_result(result: Dict[str, object], path: str = "mindbase/data/dsh_companion_train.json") -> str:
|
|
124
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
125
|
+
json.dump(result, fh, ensure_ascii=False, indent=1, default=str)
|
|
126
|
+
return path
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Dict, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class MindConfig:
|
|
9
|
+
"""All tunable parameters of the base. Units are seconds unless noted."""
|
|
10
|
+
|
|
11
|
+
seed: int = 42
|
|
12
|
+
dt: float = 60.0
|
|
13
|
+
|
|
14
|
+
# memory
|
|
15
|
+
memory_capacity: int = 128
|
|
16
|
+
memory_half_life: float = 30.0 * 86400.0 # 30 days, paper: power-law-ish decay
|
|
17
|
+
memory_min_importance: float = 0.05
|
|
18
|
+
# recency / importance / frequency / relevance
|
|
19
|
+
memory_activation_weights: Tuple[float, float, float, float] = (0.35, 0.25, 0.20, 0.20)
|
|
20
|
+
memory_retrieval: str = "bm25" # bm25 | activation
|
|
21
|
+
memory_affect_weight: float = 0.0 # 0 = off; affect-congruent retrieval bonus
|
|
22
|
+
|
|
23
|
+
# working memory
|
|
24
|
+
wm_capacity: int = 5
|
|
25
|
+
wm_decay_tau: float = 300.0
|
|
26
|
+
|
|
27
|
+
# affect / mood
|
|
28
|
+
affect_tau: float = 20.0
|
|
29
|
+
mood_tau: float = 900.0
|
|
30
|
+
stress_tau: float = 300.0
|
|
31
|
+
sleep_recovery_tau: float = 4 * 3600.0
|
|
32
|
+
sleep_stress_floor: float = 0.05
|
|
33
|
+
noise_sigma: float = 0.03
|
|
34
|
+
|
|
35
|
+
# population clock: 2h / 12h / 48h
|
|
36
|
+
clock_taus: Tuple[float, ...] = (2 * 3600.0, 12 * 3600.0, 48 * 3600.0)
|
|
37
|
+
clock_weights: Tuple[float, ...] = (0.5, 0.3, 0.2)
|
|
38
|
+
sleep_pressure_tau: float = 16 * 3600.0
|
|
39
|
+
|
|
40
|
+
# autonomy / spontaneous action
|
|
41
|
+
autonomy_tau: float = 900.0
|
|
42
|
+
autonomy_threshold: float = 0.9
|
|
43
|
+
autonomy_viability_target: float = 0.90
|
|
44
|
+
autonomy_social_target: float = 0.50
|
|
45
|
+
autonomy_activity_target: float = 0.30
|
|
46
|
+
autonomy_adapt_lr: float = 0.004
|
|
47
|
+
volition_temperature: float = 0.0 # 0 = deterministic; >0 = stochastic will
|
|
48
|
+
energy_policy_threshold: float = 0.20
|
|
49
|
+
uncertainty_gain: float = 0.0
|
|
50
|
+
chaos_r: float = 3.9
|
|
51
|
+
autonomy_cooldown: float = 180.0
|
|
52
|
+
|
|
53
|
+
# meta / abstention
|
|
54
|
+
abstain_threshold: float = 0.35
|
|
55
|
+
|
|
56
|
+
# social
|
|
57
|
+
social_tau: float = 600.0
|
|
58
|
+
contagion_strength: float = 0.15
|
|
59
|
+
|
|
60
|
+
# optional persona profile: Big Five + physiology/biology numbers
|
|
61
|
+
persona_profile: Optional[Dict[str, float]] = None
|
|
62
|
+
|
|
63
|
+
# trained heads (offline Appraisal / Policy / World)
|
|
64
|
+
appraisal_blend: float = 0.35
|
|
65
|
+
policy_prior_threshold: float = 0.55
|
|
66
|
+
world_margin: float = 0.10
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Continuous social connection state, inspired by Jiwen (积温).
|
|
2
|
+
|
|
3
|
+
Five axes are reduced to three here; valence/arousal are read from affect.
|
|
4
|
+
No LLM call; deterministic drift and thresholds.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _clip(x, lo=0.0, hi=1.0):
|
|
12
|
+
try:
|
|
13
|
+
return max(lo, min(hi, float(x)))
|
|
14
|
+
except Exception:
|
|
15
|
+
return lo
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ConnectionState:
|
|
20
|
+
connection: float = 0.0
|
|
21
|
+
pride: float = 0.35
|
|
22
|
+
immersion: float = 0.0
|
|
23
|
+
rate_mult: float = 1.0
|
|
24
|
+
force_threshold: float = 0.50
|
|
25
|
+
last_tick: float = 0.0
|
|
26
|
+
last_user: float = 0.0
|
|
27
|
+
|
|
28
|
+
def note_user(self, now: float) -> None:
|
|
29
|
+
self.connection = _clip(self.connection * 0.55 - 0.05)
|
|
30
|
+
self.pride = _clip(self.pride - 0.06, -1.0, 1.0)
|
|
31
|
+
self.last_user = float(now)
|
|
32
|
+
|
|
33
|
+
def tick(self, now: float, valence: float = 0.0, stress: float = 0.0) -> None:
|
|
34
|
+
now = float(now)
|
|
35
|
+
if self.last_tick <= 0:
|
|
36
|
+
self.last_tick = now
|
|
37
|
+
dt_min = min(180.0, max(0.0, (now - self.last_tick) / 60.0))
|
|
38
|
+
if dt_min <= 0:
|
|
39
|
+
return
|
|
40
|
+
silence_min = max(0.0, (now - self.last_user) / 60.0) if self.last_user else dt_min
|
|
41
|
+
rate = 0.0025 * max(0.1, min(3.0, float(self.rate_mult)))
|
|
42
|
+
if silence_min > 30:
|
|
43
|
+
rate *= 1.0 + min(2.0, (silence_min - 30.0) / 60.0)
|
|
44
|
+
if valence < -0.2:
|
|
45
|
+
rate *= 0.75
|
|
46
|
+
if stress > 0.7:
|
|
47
|
+
rate *= 0.8
|
|
48
|
+
self.connection = _clip(self.connection + rate * dt_min)
|
|
49
|
+
if silence_min > 10:
|
|
50
|
+
self.pride = _clip(self.pride + 0.0008 * dt_min, -1.0, 1.0)
|
|
51
|
+
else:
|
|
52
|
+
self.pride = _clip(self.pride - 0.0008 * dt_min, -1.0, 1.0)
|
|
53
|
+
self.immersion = _clip(self.immersion - 0.01 * dt_min)
|
|
54
|
+
self.last_tick = now
|
|
55
|
+
|
|
56
|
+
def trigger(self) -> str:
|
|
57
|
+
c = self.connection
|
|
58
|
+
force = max(0.25, min(0.85, float(self.force_threshold)))
|
|
59
|
+
if c >= force:
|
|
60
|
+
return "contact_force"
|
|
61
|
+
if c >= 0.35:
|
|
62
|
+
return "find_activity" if self.pride >= 0.5 else "contact"
|
|
63
|
+
if c >= 0.20:
|
|
64
|
+
return "observation"
|
|
65
|
+
return "idle"
|
|
66
|
+
|
|
67
|
+
def style_context(self) -> str:
|
|
68
|
+
c = self.connection
|
|
69
|
+
p = self.pride
|
|
70
|
+
if c < 0.20:
|
|
71
|
+
return ""
|
|
72
|
+
if c >= 0.50:
|
|
73
|
+
return "社交连接=%.2f 骄傲=%.2f 沉浸=%.2f;已经憋不住想找人,别再硬撑。" % (c, p, self.immersion)
|
|
74
|
+
if p >= 0.5:
|
|
75
|
+
return "社交连接=%.2f 骄傲=%.2f 沉浸=%.2f;想找但嘴硬,可以找点事做,别直接说想。" % (c, p, self.immersion)
|
|
76
|
+
return "社交连接=%.2f 骄傲=%.2f 沉浸=%.2f;有点想说话,但不一定非要现在开口。" % (c, p, self.immersion)
|