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.
Files changed (210) hide show
  1. package/lib/index.js +170 -27
  2. package/package.json +1 -1
  3. package/python/biocore_runtime/pipeline.py +42 -0
  4. package/python/biocore_runtime/rerank.py +201 -0
  5. package/python/mindbase/__init__.py +18 -0
  6. package/python/mindbase/__main__.py +3 -0
  7. package/python/mindbase/actions.py +56 -0
  8. package/python/mindbase/active_inference.py +54 -0
  9. package/python/mindbase/addressing.py +69 -0
  10. package/python/mindbase/affect.py +86 -0
  11. package/python/mindbase/affective.py +111 -0
  12. package/python/mindbase/allostasis.py +28 -0
  13. package/python/mindbase/appraisal.py +101 -0
  14. package/python/mindbase/attachment.py +49 -0
  15. package/python/mindbase/attention.py +30 -0
  16. package/python/mindbase/autonomy.py +81 -0
  17. package/python/mindbase/autonomy_bench.py +251 -0
  18. package/python/mindbase/autonomy_evolve.py +342 -0
  19. package/python/mindbase/autonomy_loop.py +65 -0
  20. package/python/mindbase/autonomy_meta.py +91 -0
  21. package/python/mindbase/base_only.py +28 -0
  22. package/python/mindbase/benchmark.py +61 -0
  23. package/python/mindbase/bio_adaptation.py +274 -0
  24. package/python/mindbase/bio_noise.py +39 -0
  25. package/python/mindbase/bio_semantic.py +138 -0
  26. package/python/mindbase/bm25.py +46 -0
  27. package/python/mindbase/bootstrap.py +256 -0
  28. package/python/mindbase/bootstrap_real.py +231 -0
  29. package/python/mindbase/brain.py +178 -0
  30. package/python/mindbase/care.py +78 -0
  31. package/python/mindbase/chaos_search.py +79 -0
  32. package/python/mindbase/character_profile.py +33 -0
  33. package/python/mindbase/circadian.py +38 -0
  34. package/python/mindbase/circuit.py +235 -0
  35. package/python/mindbase/clock.py +53 -0
  36. package/python/mindbase/companion_runtime.py +366 -0
  37. package/python/mindbase/companion_train.py +126 -0
  38. package/python/mindbase/config.py +66 -0
  39. package/python/mindbase/connection_state.py +76 -0
  40. package/python/mindbase/connectome_prior.py +35 -0
  41. package/python/mindbase/connectome_world.py +158 -0
  42. package/python/mindbase/constructs.py +88 -0
  43. package/python/mindbase/context_builder.py +58 -0
  44. package/python/mindbase/critical.py +36 -0
  45. package/python/mindbase/criticality.py +123 -0
  46. package/python/mindbase/defaults.py +27 -0
  47. package/python/mindbase/demo.py +163 -0
  48. package/python/mindbase/diary.py +116 -0
  49. package/python/mindbase/diary_service.py +40 -0
  50. package/python/mindbase/digital_prior.py +37 -0
  51. package/python/mindbase/diversity.py +110 -0
  52. package/python/mindbase/drives.py +106 -0
  53. package/python/mindbase/embedding.py +132 -0
  54. package/python/mindbase/embedding_memory.py +74 -0
  55. package/python/mindbase/embodiment.py +83 -0
  56. package/python/mindbase/emotion_calibration.py +39 -0
  57. package/python/mindbase/emotion_calibrator.py +50 -0
  58. package/python/mindbase/emotion_core.py +113 -0
  59. package/python/mindbase/emotion_dynamics.py +104 -0
  60. package/python/mindbase/emotion_intent.py +164 -0
  61. package/python/mindbase/emotion_layer.py +200 -0
  62. package/python/mindbase/emotion_schema.py +92 -0
  63. package/python/mindbase/engine.py +781 -0
  64. package/python/mindbase/episode_log.py +90 -0
  65. package/python/mindbase/evaluation.py +493 -0
  66. package/python/mindbase/event_appraisal.py +84 -0
  67. package/python/mindbase/evolution.py +77 -0
  68. package/python/mindbase/evolve_offline.py +232 -0
  69. package/python/mindbase/experiments.py +358 -0
  70. package/python/mindbase/expression.py +54 -0
  71. package/python/mindbase/external_brain.py +157 -0
  72. package/python/mindbase/fly_mb.py +159 -0
  73. package/python/mindbase/fly_tasks.py +45 -0
  74. package/python/mindbase/forage.py +179 -0
  75. package/python/mindbase/goal_generator.py +98 -0
  76. package/python/mindbase/goals.py +153 -0
  77. package/python/mindbase/grid_world.py +123 -0
  78. package/python/mindbase/group_autonomy.py +92 -0
  79. package/python/mindbase/group_context.py +67 -0
  80. package/python/mindbase/homeostatic.py +71 -0
  81. package/python/mindbase/homeostatic_action.py +38 -0
  82. package/python/mindbase/human_standard.py +53 -0
  83. package/python/mindbase/identity_suite.py +50 -0
  84. package/python/mindbase/image_context.py +27 -0
  85. package/python/mindbase/impression.py +119 -0
  86. package/python/mindbase/initiative_quota.py +145 -0
  87. package/python/mindbase/insight.py +51 -0
  88. package/python/mindbase/intelligence_search.py +108 -0
  89. package/python/mindbase/intention.py +76 -0
  90. package/python/mindbase/interaction_log.py +30 -0
  91. package/python/mindbase/interoception.py +67 -0
  92. package/python/mindbase/invariants.py +53 -0
  93. package/python/mindbase/iteration.py +29 -0
  94. package/python/mindbase/layered_memory.py +170 -0
  95. package/python/mindbase/layers/__init__.py +18 -0
  96. package/python/mindbase/layers/l0_substrate.py +39 -0
  97. package/python/mindbase/layers/l1_homeostasis.py +6 -0
  98. package/python/mindbase/layers/l2_affect.py +6 -0
  99. package/python/mindbase/layers/l3_attention.py +6 -0
  100. package/python/mindbase/layers/l4_learning.py +17 -0
  101. package/python/mindbase/layers/l5_self_social.py +35 -0
  102. package/python/mindbase/layers/l6_expression.py +23 -0
  103. package/python/mindbase/layers/l7_evaluation.py +45 -0
  104. package/python/mindbase/learner.py +196 -0
  105. package/python/mindbase/lexicon.py +33 -0
  106. package/python/mindbase/life_state.py +169 -0
  107. package/python/mindbase/lifecycle.py +87 -0
  108. package/python/mindbase/liquid_readout.py +48 -0
  109. package/python/mindbase/llm_adapter.py +233 -0
  110. package/python/mindbase/llm_cache.py +77 -0
  111. package/python/mindbase/llm_gate.py +85 -0
  112. package/python/mindbase/llm_openai.py +130 -0
  113. package/python/mindbase/llm_setup.py +99 -0
  114. package/python/mindbase/longrun.py +83 -0
  115. package/python/mindbase/longterm_memory.py +606 -0
  116. package/python/mindbase/mb/__init__.py +14 -0
  117. package/python/mindbase/mb/apl.py +22 -0
  118. package/python/mindbase/mb/dan.py +14 -0
  119. package/python/mindbase/mb/habituation.py +27 -0
  120. package/python/mindbase/mb/kc.py +28 -0
  121. package/python/mindbase/mb/mbon.py +23 -0
  122. package/python/mindbase/mb/mushroom_body.py +116 -0
  123. package/python/mindbase/memory.py +201 -0
  124. package/python/mindbase/memory_framework.py +158 -0
  125. package/python/mindbase/memory_retriever.py +110 -0
  126. package/python/mindbase/memory_summarizer.py +177 -0
  127. package/python/mindbase/memory_task.py +32 -0
  128. package/python/mindbase/memory_trace.py +46 -0
  129. package/python/mindbase/meta.py +32 -0
  130. package/python/mindbase/motivation.py +31 -0
  131. package/python/mindbase/multi_session.py +19 -0
  132. package/python/mindbase/narrative.py +87 -0
  133. package/python/mindbase/needs.py +77 -0
  134. package/python/mindbase/neural.py +45 -0
  135. package/python/mindbase/neuron_models.py +134 -0
  136. package/python/mindbase/noise.py +309 -0
  137. package/python/mindbase/one_on_one.py +60 -0
  138. package/python/mindbase/online_iteration.py +63 -0
  139. package/python/mindbase/ostracism.py +35 -0
  140. package/python/mindbase/parallel_eval.py +60 -0
  141. package/python/mindbase/parallel_qd.py +64 -0
  142. package/python/mindbase/persona.py +221 -0
  143. package/python/mindbase/persona_ab.py +64 -0
  144. package/python/mindbase/persona_bridge.py +129 -0
  145. package/python/mindbase/persona_compile.py +34 -0
  146. package/python/mindbase/persona_dsh.py +120 -0
  147. package/python/mindbase/persona_learner.py +157 -0
  148. package/python/mindbase/physiology.py +197 -0
  149. package/python/mindbase/population_lifecycle.py +83 -0
  150. package/python/mindbase/preferences.py +50 -0
  151. package/python/mindbase/proactive_agent.py +274 -0
  152. package/python/mindbase/proactive_gate.py +53 -0
  153. package/python/mindbase/proactive_group.py +119 -0
  154. package/python/mindbase/proactive_policy.py +422 -0
  155. package/python/mindbase/proactive_profile.py +196 -0
  156. package/python/mindbase/proactive_timing.py +28 -0
  157. package/python/mindbase/pyramid_stages.py +61 -0
  158. package/python/mindbase/quantum.py +132 -0
  159. package/python/mindbase/quantum_chaos.py +98 -0
  160. package/python/mindbase/reflection.py +68 -0
  161. package/python/mindbase/regulation_posture.py +50 -0
  162. package/python/mindbase/relationship_session.py +431 -0
  163. package/python/mindbase/relationship_suite.py +45 -0
  164. package/python/mindbase/relationships.py +70 -0
  165. package/python/mindbase/replay.py +22 -0
  166. package/python/mindbase/reply_diversity.py +53 -0
  167. package/python/mindbase/reply_gate.py +52 -0
  168. package/python/mindbase/rerank.py +41 -0
  169. package/python/mindbase/rerank_local.py +46 -0
  170. package/python/mindbase/ripple.py +187 -0
  171. package/python/mindbase/robust_search.py +68 -0
  172. package/python/mindbase/runtime.py +113 -0
  173. package/python/mindbase/scoped_identity.py +116 -0
  174. package/python/mindbase/self_insight.py +51 -0
  175. package/python/mindbase/self_model.py +116 -0
  176. package/python/mindbase/selftest.py +118 -0
  177. package/python/mindbase/semantic_memory.py +108 -0
  178. package/python/mindbase/session_store.py +175 -0
  179. package/python/mindbase/session_window.py +31 -0
  180. package/python/mindbase/shadow_eval.py +42 -0
  181. package/python/mindbase/sim_env.py +86 -0
  182. package/python/mindbase/social.py +138 -0
  183. package/python/mindbase/social_constraint.py +141 -0
  184. package/python/mindbase/social_feedback.py +95 -0
  185. package/python/mindbase/social_norm.py +225 -0
  186. package/python/mindbase/spiking_reservoir.py +204 -0
  187. package/python/mindbase/spontaneity.py +40 -0
  188. package/python/mindbase/story_loop.py +203 -0
  189. package/python/mindbase/survival_gauntlet.py +128 -0
  190. package/python/mindbase/symbolic.py +170 -0
  191. package/python/mindbase/teacher.py +244 -0
  192. package/python/mindbase/theory.py +42 -0
  193. package/python/mindbase/theory_of_mind.py +98 -0
  194. package/python/mindbase/translation_layer.py +80 -0
  195. package/python/mindbase/turn_eval.py +52 -0
  196. package/python/mindbase/types.py +78 -0
  197. package/python/mindbase/uncertainty_field.py +61 -0
  198. package/python/mindbase/user_loop.py +45 -0
  199. package/python/mindbase/user_profile.py +73 -0
  200. package/python/mindbase/value_model.py +64 -0
  201. package/python/mindbase/values.py +102 -0
  202. package/python/mindbase/vector_store.py +78 -0
  203. package/python/mindbase/viability.py +41 -0
  204. package/python/mindbase/virtual_body.py +90 -0
  205. package/python/mindbase/will.py +60 -0
  206. package/python/mindbase/working_memory.py +32 -0
  207. package/python/mindbase/workspace.py +55 -0
  208. package/ui/panel.css +16 -5
  209. package/ui/panel.html +25 -1
  210. package/ui/panel.js +57 -15
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from collections import Counter
5
+ from typing import Dict, List, Sequence
6
+
7
+
8
+ def _tokens(text: str) -> List[str]:
9
+ text = (text or "").lower()
10
+ chars = [text[i:i + 2] for i in range(max(0, len(text) - 1))]
11
+ words = [w for w in text.replace("\n", " ").split() if len(w) > 1]
12
+ return chars + words
13
+
14
+
15
+ class BM25:
16
+ def __init__(self, docs: Sequence[str], k1: float = 1.5, b: float = 0.75):
17
+ self.docs = list(docs)
18
+ self.k1 = k1
19
+ self.b = b
20
+ self.freqs: List[Counter] = [Counter(_tokens(d)) for d in self.docs]
21
+ self.lengths = [sum(f.values()) for f in self.freqs]
22
+ self.avgdl = sum(self.lengths) / max(1, len(self.lengths))
23
+ df: Dict[str, int] = {}
24
+ for f in self.freqs:
25
+ for t in f:
26
+ df[t] = df.get(t, 0) + 1
27
+ n = max(1, len(self.docs))
28
+ self.idf = {t: math.log(1 + (n - v + 0.5) / (v + 0.5)) for t, v in df.items()}
29
+
30
+ def score(self, query: str, doc_index: int) -> float:
31
+ q = Counter(_tokens(query))
32
+ f = self.freqs[doc_index]
33
+ dl = self.lengths[doc_index] or 1
34
+ total = 0.0
35
+ for term, qf in q.items():
36
+ tf = f.get(term, 0)
37
+ if not tf:
38
+ continue
39
+ denom = tf + self.k1 * (1 - self.b + self.b * dl / max(1e-6, self.avgdl))
40
+ total += self.idf.get(term, 0.0) * tf * (self.k1 + 1) / denom * qf
41
+ return total
42
+
43
+ def rank(self, query: str, k: int = 5) -> List[int]:
44
+ scores = [(self.score(query, i), i) for i in range(len(self.docs))]
45
+ scores.sort(reverse=True)
46
+ return [i for _, i in scores[:k]]
@@ -0,0 +1,256 @@
1
+ """Bootstrap local appraisal heads from silver labels, then run without the LLM.
2
+
3
+ The LLM is optional and offline-only:
4
+ texts -> DSH/LLM labeler -> StorySample dataset -> local linear AppraisalModel
5
+ Runtime _blend_appraisal() uses the local head; no LLM call.
6
+
7
+ The default ScriptedLabeler makes the pipeline testable without any API.
8
+ DSHLabeler is cache-friendly: frozen system prompt + appended JSON payload.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import math
15
+ import os
16
+ import random
17
+ import time
18
+ from pathlib import Path
19
+ from typing import Dict, List, Optional, Sequence, Tuple
20
+
21
+ from .config import MindConfig
22
+ from .engine import MindBase
23
+ from .lexicon import text_affect
24
+ from .teacher import ScriptedTeacher
25
+ from .training.appraisal import AppraisalModel, TARGETS
26
+ from .training.dataset import StorySample
27
+ from .training.features import feature_names
28
+ from .training.persist import load_heads, save_heads
29
+ from .types import Event
30
+
31
+ DATA = Path(__file__).resolve().parent / "data"
32
+ DEFAULT_HEADS = DATA / "bootstrap_heads.json"
33
+ DEFAULT_DATASET = DATA / "bootstrap_labels.jsonl"
34
+
35
+ _LABEL_SYSTEM = (
36
+ "You are an annotation teacher for a digital companion. Return ONLY JSON. "
37
+ "Rate the user text on five appraisal dimensions."
38
+ )
39
+
40
+
41
+ def _clamp(v: float, lo: float, hi: float) -> float:
42
+ try:
43
+ x = float(v)
44
+ except Exception:
45
+ return lo
46
+ return lo if x < lo else hi if x > hi else x
47
+
48
+
49
+ def _extract_json(text: str) -> Dict[str, object]:
50
+ raw = str(text or "").strip()
51
+ fence = chr(96) * 3
52
+ if raw.startswith(fence):
53
+ raw = raw.strip(chr(96))
54
+ if raw.lower().startswith("json"):
55
+ raw = raw[4:]
56
+ i = raw.find("{")
57
+ j = raw.rfind("}")
58
+ if i >= 0 and j > i:
59
+ raw = raw[i:j + 1]
60
+ return json.loads(raw)
61
+
62
+
63
+ class ScriptedLabeler:
64
+ """Deterministic silver labels; no network, no LLM. Used for tests/fallback."""
65
+
66
+ SOCIAL_WORDS = ("你", "陪我", "谢谢", "喜欢", "想", "晚安", "在吗", "我们", "一起")
67
+
68
+ def label(self, text: str, directed: bool = True, context: Optional[Dict[str, object]] = None) -> Dict[str, float]:
69
+ v, a = text_affect(str(text or ""))
70
+ social = 0.65 if directed or any(w in str(text or "") for w in self.SOCIAL_WORDS) else 0.35
71
+ relevance = 0.70 if directed else 0.45
72
+ threat = max(0.0, -float(v)) * 0.85
73
+ return {
74
+ "valence": round(_clamp(v, -1.0, 1.0), 4),
75
+ "arousal": round(_clamp(a, 0.0, 1.0), 4),
76
+ "threat": round(_clamp(threat, 0.0, 1.0), 4),
77
+ "social": round(_clamp(social, 0.0, 1.0), 4),
78
+ "relevance": round(_clamp(relevance, 0.0, 1.0), 4),
79
+ }
80
+
81
+
82
+ class DSHLabeler:
83
+ """LLM/DSH silver labeler with a stable cached prefix and scripted fallback."""
84
+
85
+ def __init__(self, client=None, fallback: Optional[ScriptedLabeler] = None):
86
+ self.client = client
87
+ self.fallback = fallback or ScriptedLabeler()
88
+ self.calls = 0
89
+
90
+ def _call(self, system: str, user: str) -> str:
91
+ if self.client is None:
92
+ raise RuntimeError("no DSH/LLM client configured")
93
+ from .llm_adapter import build_prompt
94
+ from .types import LLMRequest
95
+ req = LLMRequest(mode="semantic", urgency=0.5, max_tokens=300,
96
+ reason="bootstrap_labeler", context={"system": system, "user": user})
97
+ self.calls += 1
98
+ return str(self.client.generate(req))
99
+
100
+ def label(self, text: str, directed: bool = True, context: Optional[Dict[str, object]] = None) -> Dict[str, float]:
101
+ payload = json.dumps({
102
+ "text": str(text or ""),
103
+ "directed": bool(directed),
104
+ "context": context or {},
105
+ "schema": {k: "number" for k in TARGETS},
106
+ }, ensure_ascii=False)
107
+ try:
108
+ data = _extract_json(self._call(_LABEL_SYSTEM, payload))
109
+ out: Dict[str, float] = {}
110
+ for k in TARGETS:
111
+ lo, hi = (-1.0, 1.0) if k == "valence" else (0.0, 1.0)
112
+ out[k] = round(_clamp(data.get(k, 0.0), lo, hi), 4)
113
+ return out
114
+ except Exception:
115
+ return self.fallback.label(text, directed=directed, context=context)
116
+
117
+
118
+ def build_bootstrap_dataset(n_scenarios: int = 50, turns_per_scenario: int = 5,
119
+ labeler: Optional[object] = None, seed: int = 1,
120
+ mind_seed: int = 42) -> List[StorySample]:
121
+ labeler = labeler or ScriptedLabeler()
122
+ teacher = ScriptedTeacher(n_scenarios=max(1, int(n_scenarios)), seed=int(seed))
123
+ mind = MindBase(MindConfig(seed=int(mind_seed)))
124
+ samples: List[StorySample] = []
125
+ for i in range(max(1, int(n_scenarios))):
126
+ scenario = teacher.scenario(i)
127
+ for turn in scenario.turns[:max(1, int(turns_per_scenario))]:
128
+ event = Event(text=str(turn.text), speaker="user", directed=bool(turn.directed),
129
+ ts=mind.t + 60.0, domain="chat")
130
+ try:
131
+ mind.step(event)
132
+ except Exception:
133
+ pass
134
+ state = mind._state_features()
135
+ labels = labeler.label(str(turn.text), directed=bool(turn.directed),
136
+ context={"persona": scenario.persona, "scenario": scenario.name})
137
+ samples.append(StorySample(
138
+ event=str(turn.text),
139
+ state=dict(state),
140
+ appraisal=dict(labels),
141
+ action="speak",
142
+ outcome={},
143
+ reward=0.0,
144
+ group=str(scenario.name),
145
+ domain="bootstrap",
146
+ ))
147
+ return samples
148
+
149
+
150
+ def _split_by_group(samples: Sequence[StorySample], test_ratio: float = 0.25, seed: int = 0) -> Tuple[List[StorySample], List[StorySample]]:
151
+ groups: Dict[str, List[StorySample]] = {}
152
+ for s in samples:
153
+ groups.setdefault(s.group or "default", []).append(s)
154
+ keys = list(groups)
155
+ random.Random(int(seed)).shuffle(keys)
156
+ n_test = max(1, int(round(len(keys) * float(test_ratio)))) if keys else 0
157
+ test_keys = set(keys[:n_test])
158
+ train: List[StorySample] = []
159
+ test: List[StorySample] = []
160
+ for key, rows in groups.items():
161
+ (test if key in test_keys else train).extend(rows)
162
+ return train, test
163
+
164
+
165
+ def _mae(model: AppraisalModel, samples: Sequence[StorySample]) -> Dict[str, float]:
166
+ if not samples:
167
+ return {}
168
+ err = {k: 0.0 for k in TARGETS}
169
+ for s in samples:
170
+ pred = model.predict(s.event, s.state)
171
+ for k in TARGETS:
172
+ err[k] += abs(float(pred.get(k, 0.0)) - float(s.appraisal.get(k, 0.0)))
173
+ return {f"mae_{k}": round(v / len(samples), 4) for k, v in err.items()}
174
+
175
+
176
+ def run_bootstrap(n_scenarios: int = 50, turns_per_scenario: int = 5,
177
+ epochs: int = 120, seed: int = 1,
178
+ labeler: Optional[object] = None,
179
+ out_heads: Optional[Path | str] = None,
180
+ out_dataset: Optional[Path | str] = None,
181
+ verbose: bool = True) -> Dict[str, object]:
182
+ t0 = time.perf_counter()
183
+ labeler = labeler or ScriptedLabeler()
184
+ samples = build_bootstrap_dataset(n_scenarios=n_scenarios, turns_per_scenario=turns_per_scenario,
185
+ labeler=labeler, seed=seed)
186
+ train, test = _split_by_group(samples, test_ratio=0.25, seed=seed)
187
+ model = AppraisalModel(len(feature_names())).fit(train, epochs=int(epochs))
188
+ metrics = {
189
+ "n_total": len(samples),
190
+ "n_train": len(train),
191
+ "n_test": len(test),
192
+ "epochs": int(epochs),
193
+ "labeler": type(labeler).__name__,
194
+ "train": _mae(model, train),
195
+ "holdout": _mae(model, test),
196
+ "elapsed_sec": round(time.perf_counter() - t0, 3),
197
+ }
198
+ hp = Path(out_heads) if out_heads else DEFAULT_HEADS
199
+ dp = Path(out_dataset) if out_dataset else DEFAULT_DATASET
200
+ hp.parent.mkdir(parents=True, exist_ok=True)
201
+ save_heads({"appraisal": model}, hp)
202
+ if out_dataset is not False:
203
+ dp.parent.mkdir(parents=True, exist_ok=True)
204
+ with dp.open("w", encoding="utf-8") as f:
205
+ for s in samples:
206
+ f.write(json.dumps({
207
+ "event": s.event, "state": s.state, "appraisal": s.appraisal,
208
+ "action": s.action, "group": s.group, "domain": s.domain,
209
+ }, ensure_ascii=False) + "\n")
210
+ metrics["heads_path"] = str(hp)
211
+ metrics["dataset_path"] = str(dp)
212
+ if verbose:
213
+ print(json.dumps(metrics, ensure_ascii=False, indent=2))
214
+ return metrics
215
+
216
+
217
+ def _build_dsh_client():
218
+ try:
219
+ from .llm_openai import OpenAICompatClient
220
+ except Exception:
221
+ return None
222
+ key = os.getenv("DEEPSEEK_API_KEY") or os.getenv("LLM_API_KEY") or os.getenv("OPENAI_API_KEY")
223
+ if not key:
224
+ return None
225
+ try:
226
+ return OpenAICompatClient(model=os.getenv("LLM_MODEL") or "deepseek-chat",
227
+ base_url=os.getenv("LLM_BASE_URL") or "https://api.deepseek.com",
228
+ api_key=key, temperature=0.2)
229
+ except Exception:
230
+ return None
231
+
232
+
233
+ def main() -> None:
234
+ parser = argparse.ArgumentParser(description="Bootstrap appraisal head from silver labels (LLM optional)")
235
+ parser.add_argument("--scenarios", type=int, default=50)
236
+ parser.add_argument("--turns", type=int, default=5)
237
+ parser.add_argument("--epochs", type=int, default=120)
238
+ parser.add_argument("--seed", type=int, default=1)
239
+ parser.add_argument("--teacher", choices=["scripted", "dsh"], default="scripted")
240
+ parser.add_argument("--out-heads", default="")
241
+ parser.add_argument("--out-dataset", default="")
242
+ args = parser.parse_args()
243
+ labeler = ScriptedLabeler()
244
+ if args.teacher == "dsh":
245
+ client = _build_dsh_client()
246
+ labeler = DSHLabeler(client=client)
247
+ if client is None:
248
+ print("[bootstrap] DSH client unavailable; using scripted fallback.")
249
+ run_bootstrap(n_scenarios=args.scenarios, turns_per_scenario=args.turns,
250
+ epochs=args.epochs, seed=args.seed, labeler=labeler,
251
+ out_heads=Path(args.out_heads) if args.out_heads else None,
252
+ out_dataset=Path(args.out_dataset) if args.out_dataset else None)
253
+
254
+
255
+ if __name__ == "__main__":
256
+ main()
@@ -0,0 +1,231 @@
1
+ """Train local appraisal + timing heads from REAL datasets.
2
+
3
+ Data (already local):
4
+ EmoBank 10k -> valence/arousal
5
+ CPED 11k -> Chinese emotion/DA/Big Five
6
+ ESConv 1.3k -> support strategies
7
+ When2Speak 21.8k -> SPEAK vs SILENT timing
8
+
9
+ No LLM is called. Output: mindbase/data/bootstrap_heads_real.json
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import random
16
+ import re
17
+ import time
18
+ from collections import Counter
19
+ from pathlib import Path
20
+ from typing import Dict, List, Sequence, Tuple
21
+
22
+ from .training.appraisal import TARGETS, AppraisalModel, CompositeAppraisalModel
23
+ from .training.dataset import StorySample
24
+ from .training.features import feature_names
25
+ from .training.loaders import load_cped, load_emobank, load_esconv
26
+ from .training.persist import save_heads
27
+ from .training.timing import (TimingModel, load_human_thread_pairs, load_spontaneous_pairs,
28
+ load_when2speak_pairs)
29
+
30
+ DATA = Path(__file__).resolve().parent / "data"
31
+ REAL = DATA / "real"
32
+ DEFAULT_OUT = DATA / "bootstrap_heads_real.json"
33
+ DEFAULT_SUMMARY = DATA / "bootstrap_real_summary.json"
34
+
35
+
36
+ def _limit(n: int):
37
+ return None if int(n) <= 0 else int(n)
38
+
39
+
40
+ def load_real_data(max_per_source: int = 4000) -> Dict[str, List[StorySample]]:
41
+ lim = _limit(max_per_source)
42
+ return {
43
+ "emobank": load_emobank(REAL / "EmoBank" / "emobank.csv", limit=lim),
44
+ "cped": load_cped(REAL / "CPED" / "valid_split.csv", limit=lim),
45
+ "esconv": load_esconv(REAL / "ESConv" / "ESConv.json", limit=lim),
46
+ }
47
+
48
+
49
+ def _split_by_group(samples: Sequence[StorySample], test_ratio: float = 0.2, seed: int = 0) -> Tuple[List[StorySample], List[StorySample]]:
50
+ groups: Dict[str, List[StorySample]] = {}
51
+ for s in samples:
52
+ key = s.domain + ":" + (s.group or "default")
53
+ groups.setdefault(key, []).append(s)
54
+ keys = list(groups)
55
+ random.Random(int(seed)).shuffle(keys)
56
+ n_test = max(1, int(round(len(keys) * float(test_ratio)))) if keys else 0
57
+ test_keys = set(keys[:n_test])
58
+ train: List[StorySample] = []
59
+ test: List[StorySample] = []
60
+ for key, rows in groups.items():
61
+ (test if key in test_keys else train).extend(rows)
62
+ return train, test
63
+
64
+
65
+ def _split_appraisal(samples: Sequence[StorySample], seed: int = 0):
66
+ """Honor EmoBank train/test; group-split CPED/ESConv conversations."""
67
+ train: List[StorySample] = []
68
+ test: List[StorySample] = []
69
+ by_group: Dict[str, List[StorySample]] = {}
70
+ for s in samples:
71
+ if s.domain == "appraisal":
72
+ (test if str(s.group) == "test" else train).append(s)
73
+ else:
74
+ by_group.setdefault(s.domain + ":" + (s.group or "default"), []).append(s)
75
+ keys = list(by_group)
76
+ random.Random(int(seed)).shuffle(keys)
77
+ n_test = max(1, int(round(len(keys) * 0.2))) if keys else 0
78
+ test_keys = set(keys[:n_test])
79
+ for key, rows in by_group.items():
80
+ (test if key in test_keys else train).extend(rows)
81
+ return train, test
82
+
83
+ def _appraisal_mae(model: AppraisalModel, samples: Sequence[StorySample]) -> Dict[str, float]:
84
+ if not samples:
85
+ return {}
86
+ targets = tuple(getattr(model, "targets", TARGETS))
87
+ err = {k: 0.0 for k in targets}
88
+ for s in samples:
89
+ pred = model.predict(s.event, s.state)
90
+ for k in targets:
91
+ err[k] += abs(float(pred.get(k, 0.0)) - float(s.appraisal.get(k, 0.0)))
92
+ return {"mae_" + k: round(v / len(samples), 4) for k, v in err.items()}
93
+
94
+
95
+ def _n_speakers(context: str) -> int:
96
+ return len(set(re.findall(r"Speaker_(\d+):", str(context or ""))))
97
+
98
+ def _split_pairs(pairs: Sequence[Tuple[str, int]], test_ratio: float = 0.2, seed: int = 0):
99
+ idx = list(range(len(pairs)))
100
+ random.Random(int(seed)).shuffle(idx)
101
+ n_test = max(1, int(round(len(idx) * float(test_ratio)))) if idx else 0
102
+ test = [pairs[i] for i in idx[:n_test]]
103
+ train = [pairs[i] for i in idx[n_test:]]
104
+ return train, test
105
+
106
+
107
+ def _balance_pairs(pairs: Sequence[Tuple[str, int]], seed: int = 0, cap: int = 3000):
108
+ """Oversample the rare SPEAK class and keep all SILENT examples."""
109
+ speak = [p for p in pairs if int(p[1]) == 1]
110
+ silent = [p for p in pairs if int(p[1]) == 0]
111
+ if not speak or not silent:
112
+ return list(pairs)
113
+ mult = max(1, min(4, len(silent) // max(1, len(speak))))
114
+ out = list(silent) + speak * mult
115
+ random.Random(int(seed)).shuffle(out)
116
+ return out
117
+
118
+
119
+ def _timing_metrics(model: TimingModel, pairs) -> Dict[str, object]:
120
+ if not pairs:
121
+ return {"n": 0}
122
+ y = []
123
+ pred = []
124
+ for item in pairs:
125
+ ctx, label = item[0], int(item[1])
126
+ state = item[2] if len(item) > 2 else None
127
+ y.append(label)
128
+ pred.append(1 if model.predict(ctx, state) == "speak" else 0)
129
+ acc = sum(1 for a, b in zip(y, pred) if a == b) / len(y)
130
+ rec, prec, f1 = [], [], []
131
+ for lab in (0, 1):
132
+ tp = sum(1 for a, b in zip(y, pred) if a == lab and b == lab)
133
+ fn = sum(1 for a, b in zip(y, pred) if a == lab and b != lab)
134
+ fp = sum(1 for a, b in zip(y, pred) if a != lab and b == lab)
135
+ r = tp / (tp + fn) if (tp + fn) else 0.0
136
+ p = tp / (tp + fp) if (tp + fp) else 0.0
137
+ rec.append(r)
138
+ prec.append(p)
139
+ f1.append(2 * p * r / (p + r) if (p + r) else 0.0)
140
+ return {
141
+ "n": len(y),
142
+ "accuracy": round(acc, 4),
143
+ "balanced_accuracy": round(sum(rec) / 2.0, 4),
144
+ "macro_f1": round(sum(f1) / 2.0, 4),
145
+ "speak_recall": round(rec[1], 4),
146
+ "speak_precision": round(prec[1], 4),
147
+ "true": dict(Counter("speak" if x == 1 else "silence" for x in y)),
148
+ "pred": dict(Counter("speak" if x == 1 else "silence" for x in pred)),
149
+ }
150
+
151
+
152
+ def train_real_heads(max_per_source: int = 4000, epochs: int = 60,
153
+ seed: int = 1, out: Path | str = DEFAULT_OUT) -> Dict[str, object]:
154
+ t0 = time.perf_counter()
155
+ data = load_real_data(max_per_source=max_per_source)
156
+ va_rows = list(data["emobank"])
157
+ # CPED is the chat-domain source. ESConv is kept as a support/action
158
+ # source, not mixed into the chat appraisal head (labels/domain differ).
159
+ chat_rows = list(data["cped"])
160
+ es_rows = list(data["esconv"])
161
+ appraisal_rows = va_rows + chat_rows + es_rows
162
+ va_train, va_test = _split_appraisal(va_rows, seed=seed)
163
+ chat_train, chat_test = _split_appraisal(chat_rows, seed=seed)
164
+ _es_train, es_test = _split_appraisal(es_rows, seed=seed)
165
+ ap_test = va_test + chat_test
166
+
167
+ when2_all = load_when2speak_pairs(REAL / "When2Speak" / "finetune_val_token.jsonl")
168
+ when2_1v1 = [p for p in when2_all if _n_speakers(p[0]) <= 2]
169
+ human_all = load_human_thread_pairs(DATA / "human_thread.jsonl")
170
+ spont_all = load_spontaneous_pairs(DATA / "spontaneous_48.jsonl")
171
+ w2_train, w2_test = _split_pairs(when2_1v1, test_ratio=0.2, seed=seed)
172
+ h_train, h_test = _split_pairs(human_all, test_ratio=0.25, seed=seed)
173
+ s_train, s_test = _split_pairs(spont_all, test_ratio=0.25, seed=seed)
174
+ timing_train = w2_train + h_train * 5 + s_train * 5
175
+ timing_bal = _balance_pairs(timing_train, seed=seed, cap=3000)
176
+
177
+ n_features = len(feature_names())
178
+ va_head = AppraisalModel(n_features).fit(va_train, epochs=int(epochs))
179
+ vad_head = AppraisalModel(n_features, targets=("valence", "arousal", "dominance")).fit(va_train, epochs=int(epochs))
180
+ chat_head = AppraisalModel(n_features).fit(chat_train, epochs=int(epochs))
181
+ # The base runtime is chat; use the chat-domain head for all five targets.
182
+ # EmoBank head is kept for calibration/reporting, not runtime.
183
+ appraisal = chat_head
184
+ timing = TimingModel().fit(timing_bal, epochs=100)
185
+ timing.calibrate(h_train + s_train, metric="f1")
186
+
187
+ metrics: Dict[str, object] = {
188
+ "sources": {k: len(v) for k, v in data.items()},
189
+ "when2speak_pairs": len(when2_all),
190
+ "when2speak_1v1_pairs": len(when2_1v1),
191
+ "human_thread_pairs": len(human_all),
192
+ "spontaneous_pairs": len(spont_all),
193
+ "appraisal_rows": len(appraisal_rows),
194
+ "appraisal_train": len(va_train) + len(chat_train),
195
+ "appraisal_test": len(ap_test),
196
+ "appraisal_cped": _appraisal_mae(chat_head, chat_test),
197
+ "appraisal_esconv_generalization": _appraisal_mae(chat_head, es_test),
198
+ "appraisal_emobank": _appraisal_mae(va_head, va_test),
199
+ "appraisal_vad_emobank": _appraisal_mae(vad_head, va_test),
200
+ "timing_train": len(timing_bal),
201
+ "timing_threshold": round(float(timing.threshold), 3),
202
+ "timing_when2_1v1": _timing_metrics(timing, w2_test),
203
+ "timing_human": _timing_metrics(timing, h_test),
204
+ "timing_spontaneous": _timing_metrics(timing, s_test),
205
+ "timing_train_mix": dict(Counter("speak" if int(v) == 1 else "silence" for v in [p[1] for p in timing_bal])),
206
+ "elapsed_sec": round(time.perf_counter() - t0, 3),
207
+ }
208
+ path = Path(out)
209
+ path.parent.mkdir(parents=True, exist_ok=True)
210
+ save_heads({"appraisal": appraisal, "vad": vad_head}, path)
211
+ metrics["heads_path"] = str(path)
212
+ summary = Path(DEFAULT_SUMMARY)
213
+ summary.write_text(json.dumps(metrics, ensure_ascii=False, indent=2), encoding="utf-8")
214
+ metrics["summary_path"] = str(summary)
215
+ return metrics
216
+
217
+
218
+ def main() -> None:
219
+ parser = argparse.ArgumentParser(description="Train real appraisal + timing heads (no LLM)")
220
+ parser.add_argument("--max-per-source", type=int, default=4000)
221
+ parser.add_argument("--epochs", type=int, default=60)
222
+ parser.add_argument("--seed", type=int, default=1)
223
+ parser.add_argument("--out", default=str(DEFAULT_OUT))
224
+ args = parser.parse_args()
225
+ metrics = train_real_heads(max_per_source=args.max_per_source, epochs=args.epochs,
226
+ seed=args.seed, out=args.out)
227
+ print(json.dumps(metrics, ensure_ascii=False, indent=2))
228
+
229
+
230
+ if __name__ == "__main__":
231
+ main()