dsh-biocore 0.3.0 → 0.3.2

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.
@@ -120,6 +120,20 @@ except Exception: # pragma: no cover
120
120
  from biocore_runtime import relationship_state as _relationship_state
121
121
  except Exception:
122
122
  _relationship_state = None
123
+ try: # task-225: P0 memory + expression extensions
124
+ from . import extensions as _extensions
125
+ except Exception: # pragma: no cover
126
+ try:
127
+ from biocore_runtime import extensions as _extensions
128
+ except Exception:
129
+ _extensions = None
130
+ try: # task-224: P0 perception/relationship/self signals
131
+ from . import signals as _signals
132
+ except Exception: # pragma: no cover
133
+ try:
134
+ from biocore_runtime import signals as _signals
135
+ except Exception:
136
+ _signals = None
123
137
  try: # task-222: self layer + autonomy
124
138
  from . import self_layer as _self_layer
125
139
  except Exception: # pragma: no cover
@@ -438,6 +452,21 @@ def _concept_filter(entries, query):
438
452
  return narrowed or rows
439
453
 
440
454
 
455
+ def _signals_eval(store, session_id, user_text="", assistant_text="",
456
+ state=None, now=None):
457
+ if _signals is None or not store:
458
+ return None
459
+ try:
460
+ rel = (relationship_read(store)["relationship"]
461
+ if _store_root(store) is not None else {})
462
+ return _signals.evaluate(store, session_id=session_id,
463
+ user_text=user_text,
464
+ assistant_text=assistant_text,
465
+ state=state or {}, relationship=rel, now=now)
466
+ except Exception:
467
+ return None
468
+
469
+
441
470
  def _governance_tick(store, now=None):
442
471
  """Decay/index/graph/aggregate sidecars; never deletes a memory."""
443
472
  if _governance is None or not store:
@@ -670,6 +699,7 @@ def inject_block(session_id, store, max_chars=MAX_CHARS, top_k=TOP_K,
670
699
  text_query = clean_text(query, MAX_TEXT_CHARS)
671
700
  chosen = str(mode or "query").strip().lower()
672
701
  entries = []
702
+ query_used = False
673
703
  if chosen == "query" and text_query:
674
704
  try:
675
705
  picked = _query_entries(store, session_id, text_query, limit,
@@ -678,11 +708,24 @@ def inject_block(session_id, store, max_chars=MAX_CHARS, top_k=TOP_K,
678
708
  picked = None
679
709
  if picked:
680
710
  entries = list(picked)
711
+ query_used = True
681
712
  if not entries:
682
713
  entries = sorted(_visible(load_memory(store, session_id).get("entries")),
683
714
  key=lambda entry: (1 if entry.get("pin") else 0,
684
715
  _num(entry.get("ts"), 0.0)),
685
716
  reverse=True)[:limit]
717
+ if (query_used and text_query and entries and _extensions is not None):
718
+ try:
719
+ all_entries = _visible(load_memory(store, session_id).get("entries"))
720
+ ext = _extensions.expand_recall(
721
+ store, all_entries, text_query,
722
+ [str(entry.get("id") or "") for entry in entries])
723
+ for row in ext.get("extra") or []:
724
+ if row not in entries:
725
+ entries.append(row)
726
+ entries = entries[:max(limit, min(limit + 2, INJECT_MAX_K))]
727
+ except Exception:
728
+ pass
686
729
  header = "你记得这些事:"
687
730
  ids = []
688
731
  if not entries or budget <= len(header) + 3:
@@ -824,6 +867,22 @@ def prompt_block(payload):
824
867
  state_text = _state_block(state, budget)
825
868
  constraints = _constraints_block(state, switches, data)
826
869
  text = (state_text + "\n" + constraints).strip()
870
+ expression = None
871
+ if _extensions is not None:
872
+ try:
873
+ target = _int_param(data.get("length_target"), 0, 0, 400)
874
+ hard = _int_param(data.get("length_max"), 0, 0, 400)
875
+ expression = _extensions.expression_block(
876
+ data.get("store"), state, data.get("relationship"),
877
+ soft_target=target, soft_max=hard)
878
+ extra = [value for value in (expression.get("rules"),
879
+ expression.get("template"),
880
+ expression.get("topic"),
881
+ expression.get("voice")) if value]
882
+ if extra:
883
+ text = text + "\n" + "\n".join("- " + line for line in extra[:4])
884
+ except Exception:
885
+ expression = None
827
886
  thinking_line = thinking_style_line(data)
828
887
  persona_line = persona_prompt_line(data)
829
888
  if thinking_line:
@@ -833,6 +892,8 @@ def prompt_block(payload):
833
892
  sections = {"state": state_text, "constraints": constraints}
834
893
  if thinking_line:
835
894
  sections["thinking_style"] = thinking_line
895
+ if expression:
896
+ sections["expression"] = expression
836
897
  if persona_line:
837
898
  sections["persona"] = persona_line
838
899
  return {"ok": True, "phase": phase, "text": text, "chars": len(text),
@@ -2166,11 +2227,25 @@ def _ui_state_body(store, session_id, data):
2166
2227
  "limits": _autonomy.limits(store)}
2167
2228
  except Exception:
2168
2229
  autonomy_view = None
2230
+ signals_summary = None
2231
+ if _signals is not None:
2232
+ try:
2233
+ signals_summary = _signals.summary(store)
2234
+ except Exception:
2235
+ signals_summary = None
2236
+ extensions_summary = None
2237
+ if _extensions is not None:
2238
+ try:
2239
+ extensions_summary = _extensions.extension_summary(store)
2240
+ except Exception:
2241
+ extensions_summary = None
2169
2242
  identity = {"v": round(_num(core.get("v"), 0.0), 2),
2170
2243
  "intero": intero,
2171
2244
  "governance": governance_summary,
2172
2245
  "relationship_summary": relationship_summary,
2173
2246
  "identity": identity_summary,
2247
+ "signals": signals_summary,
2248
+ "extensions": extensions_summary,
2174
2249
  "available": True,
2175
2250
  "session_id": sanitize_session_id(session_id),
2176
2251
  "memory_count": len(entries),
@@ -2476,6 +2551,8 @@ def dispatch(action, payload=None):
2476
2551
  data.get("state"))
2477
2552
  if state_store is not None:
2478
2553
  _save_session_state(state_store, session_id, session_state)
2554
+ _signals_eval(store, session_id, user_text=user_text,
2555
+ state=session_state, now=time.time())
2479
2556
  capture_enabled = _as_bool(data.get("capture"), True)
2480
2557
  captured_user = {"captured": False, "entry_id": "", "reason": "disabled"}
2481
2558
  capture_store = data.get("store")
@@ -2607,6 +2684,51 @@ def dispatch(action, payload=None):
2607
2684
  governance_info = _governance_tick(store, now=time.time())
2608
2685
  relationship_info = _relationship_tick(
2609
2686
  store, data.get("user_text"), now=time.time())
2687
+ signals_end = _signals_eval(
2688
+ store, session_id,
2689
+ user_text=str(data.get("user_text") or ""),
2690
+ assistant_text=assistant_text,
2691
+ state=(_load_session_state(store, session_id)
2692
+ if store is not None else {}),
2693
+ now=time.time())
2694
+ if _extensions is not None:
2695
+ try:
2696
+ all_entries = _visible(
2697
+ load_memory(store, session_id).get("entries"))
2698
+ picked_ids = [str(row.get("id") or "")
2699
+ for row in all_entries[:5]]
2700
+ commitment = ((signals_end or {}).get("relationship", {})
2701
+ .get("commitment_extractor") or {})
2702
+ if commitment.get("has_commitment"):
2703
+ _extensions.add_commitment(
2704
+ store, str(commitment.get("what") or ""),
2705
+ float(commitment.get("due_ts") or 0.0),
2706
+ now=time.time())
2707
+ expression = _extensions.expression_block(
2708
+ store, _load_session_state(store, session_id),
2709
+ (relationship_read(store)["relationship"]
2710
+ if store is not None else {}))
2711
+ snapshot = {
2712
+ "graph": {"entities": 0, "edges": 0},
2713
+ "layered": _extensions.layered_view(all_entries,
2714
+ picked_ids),
2715
+ "prospective": _extensions.prospective_view(
2716
+ store, time.time()),
2717
+ "reconstruction": _extensions.reconstruction_view(
2718
+ all_entries),
2719
+ "turn_memory": _extensions.turn_memory_view(
2720
+ [{"entry_id": row.get("id"),
2721
+ "text": row.get("text"), "source": "recall"}
2722
+ for row in all_entries[:5]],
2723
+ reply=assistant_text, now=time.time()),
2724
+ "expression": expression,
2725
+ }
2726
+ hits, graph_stats = _extensions.graph_hits(
2727
+ all_entries, str(data.get("user_text") or ""))
2728
+ snapshot["graph"] = graph_stats
2729
+ _extensions.record_extension_snapshot(store, snapshot)
2730
+ except Exception:
2731
+ pass
2610
2732
  identity_info = None
2611
2733
  if relationship_info:
2612
2734
  identity_info = _identity_tick(
@@ -2677,11 +2799,18 @@ def dispatch(action, payload=None):
2677
2799
  "limits": _autonomy.limits(store)}
2678
2800
  except Exception:
2679
2801
  autonomy_info = None
2802
+ signals_info = None
2803
+ if _signals is not None:
2804
+ try:
2805
+ signals_info = _signals.summary(store)
2806
+ except Exception:
2807
+ signals_info = None
2680
2808
  return {"ok": True, "embed": embed, "switches": out,
2681
2809
  "governance": governance_info,
2682
2810
  "relationship": relationship_info,
2683
2811
  "identity": identity_info,
2684
2812
  "autonomy": autonomy_info,
2813
+ "signals": signals_info,
2685
2814
  "store": str(store)}
2686
2815
  if action == "autonomy_tick":
2687
2816
  return _autonomy_tick(store, data) or {
@@ -0,0 +1,243 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-224 (5a): P0 perception / relationship / self modules wired into the core.
3
+
4
+ One deterministic snapshot per turn, stored at <dataDir>/signals.json and
5
+ exposed through /state. Every module call is guarded: a missing or failing
6
+ module yields {"enabled": false, "reason": ...} instead of breaking a turn.
7
+ No LLM, no IO beyond the one sidecar file.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import importlib
12
+ import json
13
+ import time
14
+ from pathlib import Path
15
+
16
+ import biocore_model as _bm
17
+
18
+ SIGNALS_FILE = "signals.json"
19
+ HISTORY_LIMIT = 50
20
+ SCHEMA = 1
21
+
22
+ PERCEPTION = ("affective_labor", "compassion_fatigue", "connection_bids",
23
+ "defense_mechanisms", "emotion_geometry", "needs_events",
24
+ "social_pain", "social_repair")
25
+ RELATIONSHIP = ("commitment_extractor", "relation_stage",
26
+ "relationship_stage", "repair_after_boundary")
27
+ SELF = ("metacog", "possible_selves", "self_interest", "self_verification")
28
+
29
+
30
+ def _root(store):
31
+ if isinstance(store, dict):
32
+ raw = store.get("root") or store.get("store") or store.get("path")
33
+ else:
34
+ raw = store
35
+ text = str(raw or "").strip()
36
+ return Path(text) if text else None
37
+
38
+
39
+ def _data_dir(store):
40
+ root = _root(store)
41
+ return (root.parent if root.name == "memory" else root) if root else None
42
+
43
+
44
+ def _read(path):
45
+ try:
46
+ if path is None or not path.exists():
47
+ return {}
48
+ doc = json.loads(path.read_text(encoding="utf-8"))
49
+ return doc if isinstance(doc, dict) else {}
50
+ except Exception:
51
+ return {}
52
+
53
+
54
+ def _write(path, doc):
55
+ try:
56
+ path.parent.mkdir(parents=True, exist_ok=True)
57
+ tmp = path.with_name(path.name + ".tmp")
58
+ tmp.write_text(json.dumps(doc, ensure_ascii=False, indent=2),
59
+ encoding="utf-8")
60
+ tmp.replace(path)
61
+ return True
62
+ except Exception:
63
+ return False
64
+
65
+
66
+ def _mod(name):
67
+ module = getattr(_bm, name, None)
68
+ if module is not None:
69
+ return module
70
+ try:
71
+ return importlib.import_module("biocore_model." + name)
72
+ except Exception:
73
+ try:
74
+ return importlib.import_module("." + name, package="biocore_model")
75
+ except Exception:
76
+ return None
77
+
78
+
79
+ def _call(name, fn, *args, **kwargs):
80
+ module = _mod(name)
81
+ if module is None or not callable(getattr(module, fn, None)):
82
+ return {"enabled": False, "reason": "module_missing"}
83
+ try:
84
+ value = getattr(module, fn)(*args, **kwargs)
85
+ return {"enabled": True, "value": value}
86
+ except Exception as error:
87
+ return {"enabled": False, "reason": type(error).__name__}
88
+
89
+
90
+ def evaluate(store, session_id="s1", user_text="", assistant_text="",
91
+ state=None, relationship=None, now=None):
92
+ """Run all 17 P0 modules once; never raises."""
93
+ stamp = round(float(now or time.time()), 3)
94
+ core = state.get("core") if isinstance(state, dict) and isinstance(
95
+ state.get("core"), dict) else (state if isinstance(state, dict) else {})
96
+ rel = relationship if isinstance(relationship, dict) else {}
97
+ v = float(core.get("v") or 0.0)
98
+ a = float(core.get("a") or 0.5)
99
+ stress = float(core.get("stress") or 0.2)
100
+ social = float(core.get("social") or 0.55)
101
+ text = str(user_text or "")
102
+
103
+ out = {"schema": SCHEMA, "ts": stamp, "session_id": str(session_id),
104
+ "enabled": {}, "perception": {}, "relationship": {}, "self": {}}
105
+
106
+ # --- perception ---------------------------------------------------
107
+ labor = _call("affective_labor", "turn_cost", max(0.0, min(1.0, (v + 1) / 2)),
108
+ max(0.0, min(1.0, (v + 1) / 2)), 0.0)
109
+ out["perception"]["affective_labor"] = labor
110
+ out["enabled"]["affective_labor"] = labor["enabled"]
111
+
112
+ fatigue = _call("compassion_fatigue", "fatigue", 0, max(0.0, -v), 0.0)
113
+ out["perception"]["compassion_fatigue"] = fatigue
114
+ out["enabled"]["compassion_fatigue"] = fatigue["enabled"]
115
+
116
+ recorded = _call("connection_bids", "record_bid", [], stamp, "user")
117
+ bid_items = recorded.get("value") if isinstance(recorded.get("value"), list) else []
118
+ settled = _call("connection_bids", "settle_bids", bid_items, stamp)
119
+ view = _call("connection_bids", "bid_view", bid_items, 1.0)
120
+ out["perception"]["connection_bids"] = {
121
+ "enabled": recorded["enabled"], "bids": len(bid_items),
122
+ "settled": len(settled.get("value") or []) if isinstance(
123
+ settled.get("value"), list) else 0,
124
+ "view": view.get("value")}
125
+ out["enabled"]["connection_bids"] = recorded["enabled"]
126
+
127
+ defense = _call("defense_mechanisms", "assess",
128
+ {"kind": "message", "valence": v, "intensity": abs(v)},
129
+ {"valence": v, "arousal": a, "stress": stress})
130
+ out["perception"]["defense_mechanisms"] = defense
131
+ out["enabled"]["defense_mechanisms"] = defense["enabled"]
132
+
133
+ geometry = _call("emotion_geometry", "classify",
134
+ {"v": v, "a": a, "stress": stress})
135
+ out["perception"]["emotion_geometry"] = geometry
136
+ out["enabled"]["emotion_geometry"] = geometry["enabled"]
137
+
138
+ needs = _call("needs_events", "needs_state",
139
+ {"social": social, "energy": core.get("energy", 0.6),
140
+ "stress": stress})
141
+ out["perception"]["needs_events"] = needs
142
+ out["enabled"]["needs_events"] = needs["enabled"]
143
+
144
+ pain = _call("social_pain", "update", {"value": 0.0},
145
+ "rejection" if v < -0.3 or rel.get("conflict", 0) > 0.2
146
+ else "neutral", stamp)
147
+ pain_view = _call("social_pain", "view", pain.get("value") or {"value": 0.0})
148
+ out["perception"]["social_pain"] = {
149
+ "enabled": pain["enabled"], "view": pain_view.get("value")}
150
+ out["enabled"]["social_pain"] = pain["enabled"]
151
+
152
+ repair = _call("social_repair", "repair_check", text, "")
153
+ empathy = _call("social_repair", "empathy", v)
154
+ out["perception"]["social_repair"] = {
155
+ "enabled": repair["enabled"], "repair": repair.get("value"),
156
+ "empathy": empathy.get("value")}
157
+ out["enabled"]["social_repair"] = repair["enabled"]
158
+
159
+ # --- relationship -------------------------------------------------
160
+ commitment = _call("commitment_extractor", "extract", text, None, stamp)
161
+ value = commitment.get("value") if isinstance(commitment.get("value"), dict) else {}
162
+ out["relationship"]["commitment_extractor"] = {
163
+ "enabled": commitment["enabled"],
164
+ "has_commitment": bool(value.get("has_commitment")),
165
+ "what": str(value.get("what") or ""),
166
+ "due_ts": value.get("due_ts"),
167
+ "value": value}
168
+ out["enabled"]["commitment_extractor"] = commitment["enabled"]
169
+
170
+ stage = _call("relation_stage", "evaluate",
171
+ {"valence": v, "stress": stress, "relationship": rel})
172
+ out["relationship"]["relation_stage"] = stage
173
+ out["enabled"]["relation_stage"] = stage["enabled"]
174
+
175
+ hours = float(rel.get("days_known", 0) or 0) * 24.0
176
+ stage_word = _call("relationship_stage", "stage_for", hours)
177
+ out["relationship"]["relationship_stage"] = stage_word
178
+ out["enabled"]["relationship_stage"] = stage_word["enabled"]
179
+
180
+ boundary = _call("repair_after_boundary", "evaluate", None, 0.0, None)
181
+ out["relationship"]["repair_after_boundary"] = boundary
182
+ out["enabled"]["repair_after_boundary"] = boundary["enabled"]
183
+
184
+ # --- self ---------------------------------------------------------
185
+ band = _call("metacog", "evaluate", None,
186
+ max(0.0, min(1.0, 1.0 - abs(v))), 0,
187
+ bool(rel.get("conflict", 0) > 0.3))
188
+ out["self"]["metacog"] = band
189
+ out["enabled"]["metacog"] = band["enabled"]
190
+
191
+ selves = _call("possible_selves", "rank", None,
192
+ {"valence": v, "stress": stress})
193
+ out["self"]["possible_selves"] = selves
194
+ out["enabled"]["possible_selves"] = selves["enabled"]
195
+
196
+ interest = _call("self_interest", "assess",
197
+ {"valence": v, "social": social,
198
+ "relationship": rel, "needs": core.get("needs")})
199
+ out["self"]["self_interest"] = interest
200
+ out["enabled"]["self_interest"] = interest["enabled"]
201
+
202
+ verify = _call("self_verification", "observe", {"misread": 0.0},
203
+ 0.0, 0.6, float(rel.get("trust", 0.5) or 0.5))
204
+ verify_intent = _call("self_verification", "intent",
205
+ verify.get("value") or {"value": 0.0})
206
+ out["self"]["self_verification"] = {
207
+ "enabled": verify["enabled"], "state": verify.get("value"),
208
+ "intent": verify_intent.get("value")}
209
+ out["enabled"]["self_verification"] = verify["enabled"]
210
+
211
+ out["counts"] = {
212
+ "perception": len(out["perception"]),
213
+ "relationship": len(out["relationship"]),
214
+ "self": len(out["self"]),
215
+ "enabled": sum(1 for value in out["enabled"].values() if value),
216
+ }
217
+ _record(store, session_id, out)
218
+ return out
219
+
220
+
221
+ def _record(store, session_id, snapshot):
222
+ path = (_data_dir(store) or Path(".")) / SIGNALS_FILE
223
+ doc = _read(path)
224
+ history = [row for row in (doc.get("history") or []) if isinstance(row, dict)]
225
+ history.append(snapshot)
226
+ doc.update({"schema": SCHEMA, "ts": snapshot["ts"],
227
+ "session_id": str(session_id),
228
+ "history": history[-HISTORY_LIMIT:],
229
+ "latest": snapshot})
230
+ _write(path, doc)
231
+
232
+
233
+ def summary(store):
234
+ """Read-only snapshot for /state (never recomputes)."""
235
+ doc = _read((_data_dir(store) or Path(".")) / SIGNALS_FILE)
236
+ latest = doc.get("latest") if isinstance(doc.get("latest"), dict) else {}
237
+ if not latest:
238
+ return {"enabled": {name: False for name in
239
+ PERCEPTION + RELATIONSHIP + SELF},
240
+ "perception": {}, "relationship": {}, "self": {},
241
+ "counts": {"perception": 0, "relationship": 0, "self": 0,
242
+ "enabled": 0}}
243
+ return latest