dsh-biocore 0.1.0 → 0.1.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.
- package/README.md +2 -2
- package/lib/py/bridge.py +15 -7
- package/lib/py-bridge.js +4 -3
- package/package.json +3 -2
- package/python/biocore_memory/__init__.py +21 -0
- package/python/biocore_memory/adapters/__init__.py +2 -0
- package/python/biocore_memory/adapters/astrbot.py +24 -0
- package/python/biocore_memory/adapters/dsh_cli.py +335 -0
- package/python/biocore_memory/co_recall.py +324 -0
- package/python/biocore_memory/consolidate/__init__.py +11 -0
- package/python/biocore_memory/consolidate/gated.py +104 -0
- package/python/biocore_memory/consolidate/neighbors.py +68 -0
- package/python/biocore_memory/consolidate/reflection.py +45 -0
- package/python/biocore_memory/consolidate/tree.py +95 -0
- package/python/biocore_memory/consolidate/writeback.py +181 -0
- package/python/biocore_memory/core/__init__.py +4 -0
- package/python/biocore_memory/core/config.py +127 -0
- package/python/biocore_memory/core/permissions.py +69 -0
- package/python/biocore_memory/core/types.py +116 -0
- package/python/biocore_memory/core/util.py +26 -0
- package/python/biocore_memory/embedding_local.py +447 -0
- package/python/biocore_memory/embedding_mode.py +156 -0
- package/python/biocore_memory/engine.py +2476 -0
- package/python/biocore_memory/engine_helpers.py +266 -0
- package/python/biocore_memory/event/__init__.py +3 -0
- package/python/biocore_memory/event/_types.py +16 -0
- package/python/biocore_memory/event/consistency.py +117 -0
- package/python/biocore_memory/event/extract.py +48 -0
- package/python/biocore_memory/event/media.py +111 -0
- package/python/biocore_memory/event/notes.py +85 -0
- package/python/biocore_memory/event/notes_link.py +87 -0
- package/python/biocore_memory/event/vector.py +69 -0
- package/python/biocore_memory/evict/__init__.py +3 -0
- package/python/biocore_memory/evict/adaptive.py +73 -0
- package/python/biocore_memory/evict/consequence.py +58 -0
- package/python/biocore_memory/evict/heat.py +54 -0
- package/python/biocore_memory/evict/inhibition.py +105 -0
- package/python/biocore_memory/evict/policy.py +188 -0
- package/python/biocore_memory/evict/score.py +23 -0
- package/python/biocore_memory/identity/__init__.py +6 -0
- package/python/biocore_memory/identity/api.py +99 -0
- package/python/biocore_memory/identity/history.py +9 -0
- package/python/biocore_memory/identity/ledger.py +191 -0
- package/python/biocore_memory/identity/narrative.py +358 -0
- package/python/biocore_memory/identity/snapshot.py +13 -0
- package/python/biocore_memory/identity/writeback.py +61 -0
- package/python/biocore_memory/index/__init__.py +3 -0
- package/python/biocore_memory/index/cue_index.py +80 -0
- package/python/biocore_memory/index/entities.py +85 -0
- package/python/biocore_memory/index/entity_boost.py +104 -0
- package/python/biocore_memory/index/graph.py +172 -0
- package/python/biocore_memory/recall/__init__.py +5 -0
- package/python/biocore_memory/recall/api.py +65 -0
- package/python/biocore_memory/recall/bm25.py +118 -0
- package/python/biocore_memory/recall/channels.py +98 -0
- package/python/biocore_memory/recall/complexity.py +62 -0
- package/python/biocore_memory/recall/constraints.py +75 -0
- package/python/biocore_memory/recall/embedding.py +745 -0
- package/python/biocore_memory/recall/expand.py +26 -0
- package/python/biocore_memory/recall/filters.py +40 -0
- package/python/biocore_memory/recall/fusion.py +18 -0
- package/python/biocore_memory/recall/mmr.py +70 -0
- package/python/biocore_memory/recall/ppr.py +85 -0
- package/python/biocore_memory/recall/retrieval_practice.py +102 -0
- package/python/biocore_memory/relay/__init__.py +2 -0
- package/python/biocore_memory/relay/card.py +51 -0
- package/python/biocore_memory/slots/__init__.py +18 -0
- package/python/biocore_memory/slots/admission.py +64 -0
- package/python/biocore_memory/slots/boundary.py +128 -0
- package/python/biocore_memory/slots/evict_bridge.py +106 -0
- package/python/biocore_memory/slots/registry.py +135 -0
- package/python/biocore_memory/slots/switch.py +42 -0
- package/python/biocore_memory/store/__init__.py +4 -0
- package/python/biocore_memory/store/audit.py +74 -0
- package/python/biocore_memory/store/compress.py +81 -0
- package/python/biocore_memory/store/dedupe.py +178 -0
- package/python/biocore_memory/store/json_store.py +154 -0
- package/python/biocore_memory/store/media.py +145 -0
- package/python/biocore_memory/store/provenance.py +73 -0
- package/python/biocore_memory/store/rate_limit.py +93 -0
- package/python/biocore_memory/store/security.py +107 -0
- package/python/biocore_memory/store/source_consistency.py +123 -0
- package/python/biocore_memory/store/tiering.py +61 -0
- package/python/biocore_memory/store/tiering_exec.py +50 -0
- package/python/biocore_memory/store/tombstone.py +23 -0
- package/python/biocore_memory/store/versions.py +69 -0
- package/python/biocore_memory/thread/__init__.py +2 -0
- package/python/biocore_memory/thread/card.py +58 -0
- package/python/biocore_model/__init__.py +37 -0
- package/python/biocore_model/affective_labor.py +162 -0
- package/python/biocore_model/appraisal.py +257 -0
- package/python/biocore_model/appraisal_action.py +78 -0
- package/python/biocore_model/archive_queue.py +60 -0
- package/python/biocore_model/attachment_behavior.py +130 -0
- package/python/biocore_model/attachment_regulation.py +75 -0
- package/python/biocore_model/attachment_state.py +41 -0
- package/python/biocore_model/attribution.py +73 -0
- package/python/biocore_model/attributor.py +162 -0
- package/python/biocore_model/autonomic.py +91 -0
- package/python/biocore_model/autonomy_affect.py +51 -0
- package/python/biocore_model/autonomy_check.py +67 -0
- package/python/biocore_model/autonomy_loop.py +122 -0
- package/python/biocore_model/autonomy_mode.py +114 -0
- package/python/biocore_model/autonomy_model.py +733 -0
- package/python/biocore_model/autonomy_params.py +131 -0
- package/python/biocore_model/autonomy_reason.py +73 -0
- package/python/biocore_model/autonomy_tick.py +227 -0
- package/python/biocore_model/bio_appraisal.py +49 -0
- package/python/biocore_model/bio_autonomy_state.py +171 -0
- package/python/biocore_model/bio_config.py +221 -0
- package/python/biocore_model/bio_const.py +128 -0
- package/python/biocore_model/bio_daily.py +267 -0
- package/python/biocore_model/bio_emotion.py +538 -0
- package/python/biocore_model/bio_emotion2.py +214 -0
- package/python/biocore_model/bio_goal.py +220 -0
- package/python/biocore_model/bio_identity.py +82 -0
- package/python/biocore_model/bio_noise.py +119 -0
- package/python/biocore_model/bio_physiology2.py +114 -0
- package/python/biocore_model/bio_snapshot.py +324 -0
- package/python/biocore_model/bio_state.py +272 -0
- package/python/biocore_model/bio_tick.py +310 -0
- package/python/biocore_model/bio_util.py +82 -0
- package/python/biocore_model/blocks.py +264 -0
- package/python/biocore_model/boundary.py +251 -0
- package/python/biocore_model/boundary_gate.py +188 -0
- package/python/biocore_model/commitment_extractor.py +517 -0
- package/python/biocore_model/compassion_fatigue.py +84 -0
- package/python/biocore_model/connection_bids.py +120 -0
- package/python/biocore_model/core_bridge.py +177 -0
- package/python/biocore_model/core_client.py +127 -0
- package/python/biocore_model/core_commands.py +112 -0
- package/python/biocore_model/core_mode.py +104 -0
- package/python/biocore_model/core_mode_ui.mjs +104 -0
- package/python/biocore_model/criticality.py +81 -0
- package/python/biocore_model/defense_mechanisms.py +272 -0
- package/python/biocore_model/delta_budget.py +99 -0
- package/python/biocore_model/diag_trace.py +274 -0
- package/python/biocore_model/dissonance.py +134 -0
- package/python/biocore_model/emotion_geometry.py +108 -0
- package/python/biocore_model/emotion_probe.py +209 -0
- package/python/biocore_model/entity_graph.py +931 -0
- package/python/biocore_model/entity_resolve.py +260 -0
- package/python/biocore_model/envy.py +193 -0
- package/python/biocore_model/exit_or_silence.py +230 -0
- package/python/biocore_model/expr_guard.py +24 -0
- package/python/biocore_model/expr_intent.py +77 -0
- package/python/biocore_model/expr_state.py +204 -0
- package/python/biocore_model/expr_template.py +105 -0
- package/python/biocore_model/expr_tone.py +81 -0
- package/python/biocore_model/extreme_scenarios.py +205 -0
- package/python/biocore_model/feedback_flow.py +227 -0
- package/python/biocore_model/feedback_guard.py +129 -0
- package/python/biocore_model/feedback_llm.py +163 -0
- package/python/biocore_model/feedback_memory.py +96 -0
- package/python/biocore_model/feedback_user.py +93 -0
- package/python/biocore_model/frustration_curve.py +110 -0
- package/python/biocore_model/granularity.py +283 -0
- package/python/biocore_model/graph_extract.py +248 -0
- package/python/biocore_model/graph_recall.py +221 -0
- package/python/biocore_model/intent_render.py +91 -0
- package/python/biocore_model/intero_core.py +235 -0
- package/python/biocore_model/intero_style.py +116 -0
- package/python/biocore_model/layered_memory.py +297 -0
- package/python/biocore_model/learning_probe.py +120 -0
- package/python/biocore_model/memory_dual.py +224 -0
- package/python/biocore_model/memory_forgetting.py +125 -0
- package/python/biocore_model/memory_store.py +1399 -0
- package/python/biocore_model/metacog.py +153 -0
- package/python/biocore_model/migrate.py +733 -0
- package/python/biocore_model/migrate_store.py +337 -0
- package/python/biocore_model/mind_wander.py +111 -0
- package/python/biocore_model/mood.py +48 -0
- package/python/biocore_model/narrative.py +172 -0
- package/python/biocore_model/needs_esteem.py +116 -0
- package/python/biocore_model/needs_events.py +137 -0
- package/python/biocore_model/needs_sleep.py +72 -0
- package/python/biocore_model/noise_bank.py +95 -0
- package/python/biocore_model/persona_flow.mjs +25 -0
- package/python/biocore_model/pipeline.py +240 -0
- package/python/biocore_model/pipeline_api.py +200 -0
- package/python/biocore_model/plugin_config.py +83 -0
- package/python/biocore_model/plugin_routes.py +1435 -0
- package/python/biocore_model/policy_learning.py +132 -0
- package/python/biocore_model/possible_selves.py +110 -0
- package/python/biocore_model/predictor.py +178 -0
- package/python/biocore_model/preset.py +205 -0
- package/python/biocore_model/proactive_context.py +153 -0
- package/python/biocore_model/prompt_rules.py +90 -0
- package/python/biocore_model/prospective.py +183 -0
- package/python/biocore_model/provider_fallback.py +97 -0
- package/python/biocore_model/reconstruction.py +132 -0
- package/python/biocore_model/regulation_loop.py +179 -0
- package/python/biocore_model/regulation_strategies.py +294 -0
- package/python/biocore_model/relation_stage.py +130 -0
- package/python/biocore_model/relationship_stage.py +95 -0
- package/python/biocore_model/render.py +901 -0
- package/python/biocore_model/render_guard.mjs +34 -0
- package/python/biocore_model/repair_after_boundary.py +350 -0
- package/python/biocore_model/reply_limit.py +183 -0
- package/python/biocore_model/reply_split.py +203 -0
- package/python/biocore_model/rollback.py +143 -0
- package/python/biocore_model/runtime.py +479 -0
- package/python/biocore_model/sanitize.py +200 -0
- package/python/biocore_model/scheduler.py +123 -0
- package/python/biocore_model/schema.py +120 -0
- package/python/biocore_model/secrecy.py +145 -0
- package/python/biocore_model/self_esteem.py +50 -0
- package/python/biocore_model/self_intent.py +437 -0
- package/python/biocore_model/self_interest.py +263 -0
- package/python/biocore_model/self_verification.py +178 -0
- package/python/biocore_model/services.py +78 -0
- package/python/biocore_model/situation.py +254 -0
- package/python/biocore_model/sleep_circadian.py +116 -0
- package/python/biocore_model/social_pain.py +69 -0
- package/python/biocore_model/social_repair.py +111 -0
- package/python/biocore_model/social_theory.py +131 -0
- package/python/biocore_model/source_monitoring.py +292 -0
- package/python/biocore_model/stages.py +487 -0
- package/python/biocore_model/state_tag.py +231 -0
- package/python/biocore_model/switch_groups.mjs +201 -0
- package/python/biocore_model/tabs_state.mjs +37 -0
- package/python/biocore_model/template_expr.py +252 -0
- package/python/biocore_model/tools/caps_check.mjs +23 -0
- package/python/biocore_model/tools/init_symbols_check.mjs +11 -0
- package/python/biocore_model/tools/missing_ids.mjs +16 -0
- package/python/biocore_model/tools/scope_check.mjs +19 -0
- package/python/biocore_model/topic_control.py +268 -0
- package/python/biocore_model/turn_memory.py +152 -0
- package/python/biocore_model/ui_compat.mjs +48 -0
- package/python/biocore_model/ui_settings.py +266 -0
- package/python/biocore_model/ui_settings.py.bak_authority +108 -0
- package/python/biocore_model/value_order.py +186 -0
- package/python/biocore_model/view_mode.mjs +8 -0
- package/python/biocore_model/voice_bank.py +177 -0
- package/python/biocore_model/waiting_sense.py +229 -0
- package/python/biocore_model/x_directives.py +142 -0
- package/python/biocore_runtime/__init__.py +7 -0
- package/python/biocore_runtime/pipeline.py +1915 -0
- package/python/dsh_adapter/README.md +96 -0
- package/python/dsh_adapter/adapter.py +1077 -0
- package/python/dsh_adapter/cli.py +107 -0
- package/python/dsh_adapter/data/demo_state.json +164 -0
- package/python/dsh_adapter/data/verify_state.json +164 -0
- package/python/dsh_adapter/tests/__init__.py +1 -0
- package/python/dsh_adapter/tests/test_adapter.py +494 -0
- package/ui/panel-layout.js +3 -3
- package/ui/panel.css +53 -6
- package/ui/panel.html +33 -32
- package/ui/panel.js +9 -1
- package/lib/py/__pycache__/bridge.cpython-310.pyc +0 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Graph recall -> "你知道的" prompt block + panel data (task-136).
|
|
3
|
+
|
|
4
|
+
Read-only: match entity names/aliases in the current message, walk 1-2 hops
|
|
5
|
+
in the SQLite entity graph, rank edges by recency and confidence, and render
|
|
6
|
+
at most five lines. A time window ("上周"/"昨天"/"三天前") restricts edges to
|
|
7
|
+
that window. panel_data() feeds the Graph dashboard page.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
from . import entity_graph as _eg
|
|
15
|
+
from . import graph_extract as _gx
|
|
16
|
+
|
|
17
|
+
MAX_LINES = 5
|
|
18
|
+
MAX_ENTITIES = 3
|
|
19
|
+
MAX_EDGES = 200
|
|
20
|
+
|
|
21
|
+
WINDOW_WORDS = {
|
|
22
|
+
"上周": 7, "上星期": 7, "上礼拜": 7, "最近一周": 7, "这周": 7,
|
|
23
|
+
"本周": 7, "这星期": 7, "昨天": 1, "昨日": 1, "前天": 2,
|
|
24
|
+
"三天内": 3, "最近三天": 3, "上个月": 30, "上月": 30,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def detect_window(text, now=None):
|
|
29
|
+
"""Return (window_days, label) for a relative time phrase, else (None, "")."""
|
|
30
|
+
del now
|
|
31
|
+
value = " ".join(str(text or "").split())
|
|
32
|
+
for word, days in WINDOW_WORDS.items():
|
|
33
|
+
if word in value:
|
|
34
|
+
return int(days), word
|
|
35
|
+
return None, ""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _fmt_ts(ts):
|
|
39
|
+
number = float(ts or 0.0)
|
|
40
|
+
if number <= 0:
|
|
41
|
+
return ""
|
|
42
|
+
try:
|
|
43
|
+
return time.strftime("%m-%d %H:%M", time.localtime(number))
|
|
44
|
+
except Exception:
|
|
45
|
+
return ""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _rel_score(edge, now):
|
|
49
|
+
try:
|
|
50
|
+
confidence = float(edge.get("confidence", 0.0) or 0.0)
|
|
51
|
+
except Exception:
|
|
52
|
+
confidence = 0.0
|
|
53
|
+
try:
|
|
54
|
+
ts = float(edge.get("ts", 0.0) or 0.0)
|
|
55
|
+
except Exception:
|
|
56
|
+
ts = 0.0
|
|
57
|
+
if ts <= 0 or now <= 0:
|
|
58
|
+
recency = 0.0
|
|
59
|
+
else:
|
|
60
|
+
age_days = max(0.0, (now - ts) / 86400.0)
|
|
61
|
+
recency = 1.0 / (1.0 + age_days / 7.0)
|
|
62
|
+
return round(0.65 * confidence + 0.35 * recency, 4)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _name_of(by_id, entity_id):
|
|
66
|
+
row = by_id.get(entity_id)
|
|
67
|
+
if isinstance(row, dict):
|
|
68
|
+
return str(row.get("name") or "")
|
|
69
|
+
return ""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def recall(store, text, now=None, hops=2, limit=MAX_LINES,
|
|
73
|
+
window_days=None, window_label=""):
|
|
74
|
+
"""Rank graph edges around matched entities -> list of prompt rows."""
|
|
75
|
+
del window_label
|
|
76
|
+
stamp = float(now if now is not None else time.time())
|
|
77
|
+
entities = _eg.search(store, text, limit=MAX_ENTITIES)
|
|
78
|
+
if not entities:
|
|
79
|
+
return []
|
|
80
|
+
try:
|
|
81
|
+
cap = max(1, min(MAX_LINES, int(limit)))
|
|
82
|
+
except Exception:
|
|
83
|
+
cap = MAX_LINES
|
|
84
|
+
if window_days is None:
|
|
85
|
+
window_days, _label = detect_window(text, stamp)
|
|
86
|
+
by_id = {entity["id"]: entity for entity in entities}
|
|
87
|
+
candidate_edges = []
|
|
88
|
+
seen_edges = set()
|
|
89
|
+
for entity in entities:
|
|
90
|
+
walked = _eg.neighbors(store, entity["name"], hops=hops,
|
|
91
|
+
window_days=window_days, now=stamp)
|
|
92
|
+
for row in walked.get("entities") or []:
|
|
93
|
+
by_id.setdefault(row["id"], row)
|
|
94
|
+
for edge in walked.get("edges") or []:
|
|
95
|
+
eid = str(edge.get("id") or "")
|
|
96
|
+
if not eid or eid in seen_edges:
|
|
97
|
+
continue
|
|
98
|
+
seen_edges.add(eid)
|
|
99
|
+
candidate_edges.append(edge)
|
|
100
|
+
rows = []
|
|
101
|
+
for edge in candidate_edges[:MAX_EDGES]:
|
|
102
|
+
src_name = _name_of(by_id, edge.get("src"))
|
|
103
|
+
dst_name = _name_of(by_id, edge.get("dst"))
|
|
104
|
+
if not src_name or not dst_name:
|
|
105
|
+
continue
|
|
106
|
+
relation = str(edge.get("rel") or "")
|
|
107
|
+
line = "%s —%s→ %s" % (src_name, _gx.rel_label(relation), dst_name)
|
|
108
|
+
when = _fmt_ts(edge.get("ts"))
|
|
109
|
+
if when:
|
|
110
|
+
line += "(%s)" % when
|
|
111
|
+
source_entity = by_id.get(edge.get("src")) or {}
|
|
112
|
+
target_entity = by_id.get(edge.get("dst")) or {}
|
|
113
|
+
rows.append({"edge_id": edge.get("id"), "line": line,
|
|
114
|
+
"src": src_name, "dst": dst_name, "rel": relation,
|
|
115
|
+
"rel_label": _gx.rel_label(relation),
|
|
116
|
+
"ts": float(edge.get("ts", 0.0) or 0.0),
|
|
117
|
+
"confidence": float(edge.get("confidence", 0.0) or 0.0),
|
|
118
|
+
"score": _rel_score(edge, stamp),
|
|
119
|
+
"src_type": source_entity.get("type"),
|
|
120
|
+
"dst_type": target_entity.get("type"),
|
|
121
|
+
"source_entry": str(edge.get("source_entry") or "")})
|
|
122
|
+
rows.sort(key=lambda row: (row["score"], row["ts"]), reverse=True)
|
|
123
|
+
return rows[:cap]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def render(store, text, now=None, limit=MAX_LINES):
|
|
127
|
+
"""Return the "你知道的" block (<=5 lines) or an empty string."""
|
|
128
|
+
try:
|
|
129
|
+
rows = recall(store, text, now=now, limit=limit)
|
|
130
|
+
except Exception:
|
|
131
|
+
return ""
|
|
132
|
+
if not rows:
|
|
133
|
+
return ""
|
|
134
|
+
lines = ["你知道的:"]
|
|
135
|
+
for row in rows:
|
|
136
|
+
lines.append("- " + str(row["line"]))
|
|
137
|
+
return "\n".join(lines[:max(1, min(MAX_LINES, int(limit))) + 1])
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _row_entity_public(row):
|
|
141
|
+
def _json_list(raw):
|
|
142
|
+
try:
|
|
143
|
+
value = json.loads(raw or "[]")
|
|
144
|
+
except Exception:
|
|
145
|
+
return []
|
|
146
|
+
return [str(x) for x in value if str(x)] if isinstance(value, list) else []
|
|
147
|
+
|
|
148
|
+
kind = str(row[2] or "topic")
|
|
149
|
+
return {"id": row[0], "name": row[1], "type": kind,
|
|
150
|
+
"type_label": _gx.type_label(kind),
|
|
151
|
+
"aliases": _json_list(row[3]),
|
|
152
|
+
"first_seen": float(row[4] or 0.0),
|
|
153
|
+
"last_seen": float(row[5] or 0.0),
|
|
154
|
+
"sources": _json_list(row[6])}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def timeline(store, entity, now=None, window_days=None, limit=12):
|
|
158
|
+
"""Edges touching one entity, newest first (panel detail / time view)."""
|
|
159
|
+
stamp = float(now if now is not None else time.time())
|
|
160
|
+
try:
|
|
161
|
+
cap = max(1, min(50, int(limit)))
|
|
162
|
+
except Exception:
|
|
163
|
+
cap = 12
|
|
164
|
+
walked = _eg.neighbors(store, entity, hops=1, window_days=window_days,
|
|
165
|
+
now=stamp)
|
|
166
|
+
by_id = {row["id"]: row for row in walked.get("entities") or []}
|
|
167
|
+
rows = []
|
|
168
|
+
for edge in walked.get("edges") or []:
|
|
169
|
+
src_name = _name_of(by_id, edge.get("src"))
|
|
170
|
+
dst_name = _name_of(by_id, edge.get("dst"))
|
|
171
|
+
rows.append({"edge_id": edge.get("id"),
|
|
172
|
+
"text": "%s —%s→ %s" % (
|
|
173
|
+
src_name, _gx.rel_label(edge.get("rel")), dst_name),
|
|
174
|
+
"when": _fmt_ts(edge.get("ts")),
|
|
175
|
+
"ts": float(edge.get("ts", 0.0) or 0.0),
|
|
176
|
+
"confidence": float(edge.get("confidence", 0.0) or 0.0)})
|
|
177
|
+
rows.sort(key=lambda row: row["ts"], reverse=True)
|
|
178
|
+
return rows[:cap]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def panel_data(store, now=None, limit_entities=100, limit_edges=400):
|
|
182
|
+
"""Flat entity/edge snapshot consumed by the dashboard Graph page."""
|
|
183
|
+
stamp = float(now if now is not None else time.time())
|
|
184
|
+
try:
|
|
185
|
+
entity_cap = max(1, min(500, int(limit_entities)))
|
|
186
|
+
edge_cap = max(1, min(2000, int(limit_edges)))
|
|
187
|
+
except Exception:
|
|
188
|
+
entity_cap, edge_cap = 100, 400
|
|
189
|
+
entities = [_row_entity_public(row) for row in store._conn.execute(
|
|
190
|
+
"SELECT id, name, type, aliases, first_seen, last_seen, sources "
|
|
191
|
+
"FROM entities ORDER BY last_seen DESC LIMIT ?",
|
|
192
|
+
(entity_cap,)).fetchall()]
|
|
193
|
+
by_id = {row["id"]: row for row in entities}
|
|
194
|
+
edges = []
|
|
195
|
+
for row in store._conn.execute(
|
|
196
|
+
"SELECT id, src, rel, dst, ts, confidence, source_entry "
|
|
197
|
+
"FROM edges ORDER BY ts DESC LIMIT ?", (edge_cap,)).fetchall():
|
|
198
|
+
edges.append({"id": row[0], "src": row[1], "dst": row[3],
|
|
199
|
+
"src_name": (by_id.get(row[1]) or {}).get("name", ""),
|
|
200
|
+
"dst_name": (by_id.get(row[3]) or {}).get("name", ""),
|
|
201
|
+
"rel": row[2], "rel_label": _gx.rel_label(row[2]),
|
|
202
|
+
"ts": float(row[4] or 0.0),
|
|
203
|
+
"when": _fmt_ts(row[4]),
|
|
204
|
+
"confidence": float(row[5] or 0.0),
|
|
205
|
+
"source_entry": row[6] or ""})
|
|
206
|
+
try:
|
|
207
|
+
stats = _eg.stats(store)
|
|
208
|
+
except Exception:
|
|
209
|
+
stats = {"entities": len(entities), "edges": len(edges)}
|
|
210
|
+
return {"stats": stats, "entities": entities, "edges": edges,
|
|
211
|
+
"now": stamp}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def source_entries(edges):
|
|
215
|
+
"""Collect source entry ids for the panel's "related entries" column."""
|
|
216
|
+
seen = []
|
|
217
|
+
for edge in edges or []:
|
|
218
|
+
value = str((edge or {}).get("source_entry") or "")
|
|
219
|
+
if value and value not in seen:
|
|
220
|
+
seen.append(value)
|
|
221
|
+
return seen
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""HUMAN-6 intent rendering: feeling -> tendency constraint, no raw signals.
|
|
3
|
+
|
|
4
|
+
Turns an internal action/feeling into one short tendency line for the
|
|
5
|
+
interpretation layer. Hard constraints: no digits, no emotion words, no
|
|
6
|
+
diagnostic labels -- only a behavioural inclination plus optional tone/guard
|
|
7
|
+
fragments that are themselves sanitized.
|
|
8
|
+
|
|
9
|
+
Literature
|
|
10
|
+
Barrett 2017 (language constrains constructed emotion), Craig 2009
|
|
11
|
+
(interoception precedes words), Seth 2013 (prediction -> action tendency),
|
|
12
|
+
Powers 1973 (perceptual control error becomes action), Carver & Scheier 1998
|
|
13
|
+
(approach/withdraw tendencies), Gross 1998 (regulation strategy phrasing),
|
|
14
|
+
Garfinkel 2015 (sensibility vs accuracy), Sterling 2012 (allostatic action).
|
|
15
|
+
|
|
16
|
+
Constraints: pure functions, stdlib only, no IO, no main import.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
|
|
22
|
+
PHRASES = {
|
|
23
|
+
"approach": "想靠近一点",
|
|
24
|
+
"share": "想和你说说话",
|
|
25
|
+
"question": "想先问清楚",
|
|
26
|
+
"withdraw": "想先退开一点",
|
|
27
|
+
"repair": "想把气氛缓一缓",
|
|
28
|
+
"rest": "想安静一会儿",
|
|
29
|
+
}
|
|
30
|
+
FALLBACK = "想待一会儿"
|
|
31
|
+
FORBIDDEN_EMOTION_WORDS = (
|
|
32
|
+
"开心", "高兴", "快乐", "难过", "伤心", "悲伤", "生气", "愤怒", "恼火",
|
|
33
|
+
"害怕", "恐惧", "焦虑", "紧张", "孤独", "寂寞", "压力", "情绪", "心情",
|
|
34
|
+
"抑郁", "烦躁", "崩溃", "委屈", "内疚", "羞耻",
|
|
35
|
+
)
|
|
36
|
+
DIGIT_RE = re.compile(r"[0-90-9]+")
|
|
37
|
+
PUNCT_RE = re.compile(r"[,,。;;、\s]+")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _clean_fragment(value):
|
|
41
|
+
text = str(value or "")
|
|
42
|
+
text = DIGIT_RE.sub("", text)
|
|
43
|
+
for word in FORBIDDEN_EMOTION_WORDS:
|
|
44
|
+
text = text.replace(word, "")
|
|
45
|
+
return PUNCT_RE.sub(" ", text).strip(" ,,。;;、")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def check(text):
|
|
49
|
+
"""Return {ok, digits, emotion_words} for one candidate line."""
|
|
50
|
+
value = str(text or "")
|
|
51
|
+
digits = DIGIT_RE.findall(value)
|
|
52
|
+
words = [word for word in FORBIDDEN_EMOTION_WORDS if word in value]
|
|
53
|
+
return {"ok": not digits and not words, "digits": digits, "emotion_words": words}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def sanitize(text, fallback=FALLBACK):
|
|
57
|
+
value = _clean_fragment(text)
|
|
58
|
+
if not value:
|
|
59
|
+
value = fallback
|
|
60
|
+
return value
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def render(intent, tone=None, guard=None):
|
|
64
|
+
"""Render one tendency line: action phrase + optional tone/guard."""
|
|
65
|
+
action = ""
|
|
66
|
+
if isinstance(intent, dict):
|
|
67
|
+
action = str(intent.get("action") or "")
|
|
68
|
+
elif intent:
|
|
69
|
+
action = str(intent)
|
|
70
|
+
phrase = PHRASES.get(action, FALLBACK)
|
|
71
|
+
parts = [phrase]
|
|
72
|
+
tone_text = _clean_fragment(tone)
|
|
73
|
+
if tone_text:
|
|
74
|
+
parts.append(tone_text)
|
|
75
|
+
guard_text = _clean_fragment(guard)
|
|
76
|
+
if guard_text:
|
|
77
|
+
parts.append(guard_text)
|
|
78
|
+
text = ",".join(parts)
|
|
79
|
+
text = sanitize(text)
|
|
80
|
+
result = check(text)
|
|
81
|
+
return text if result["ok"] else sanitize(text)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def tendency(intent):
|
|
85
|
+
"""Coarse approach/withdraw label for downstream stages (no words)."""
|
|
86
|
+
action = ""
|
|
87
|
+
if isinstance(intent, dict):
|
|
88
|
+
action = str(intent.get("action") or "")
|
|
89
|
+
return {"approach": "approach", "share": "approach", "question": "approach",
|
|
90
|
+
"repair": "approach", "withdraw": "withdraw", "rest": "withdraw"}.get(
|
|
91
|
+
action, "neutral")
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""L0 interoception core: four non-verbal body dimensions in [0, 1].
|
|
3
|
+
|
|
4
|
+
These values are computed from body/affect state only -- never inferred from
|
|
5
|
+
text -- so the biological layer keeps owning "how the body feels" while the
|
|
6
|
+
interpretation layer owns language.
|
|
7
|
+
|
|
8
|
+
Dimensions
|
|
9
|
+
tension (绷紧): stress/arousal load, sleep debt, relationship conflict
|
|
10
|
+
warmth (暖) : valence baseline, social saturation, trust, closeness
|
|
11
|
+
heaviness (沉) : sadness, allostatic load, low energy, loneliness
|
|
12
|
+
clarity (清) : inverse noise, restedness, arousal near the middle
|
|
13
|
+
|
|
14
|
+
Literature
|
|
15
|
+
Craig 2009 (insula interoception), Barrett 2017 (constructed emotion),
|
|
16
|
+
Seth 2013 (interoceptive prediction), Garfinkel 2015 (three-dimensional
|
|
17
|
+
interoception: accuracy/sensibility/awareness), Sterling 2012 (allostasis),
|
|
18
|
+
Carver & Scheier 1998 (cybernetic control), Powers 1973 (perceptual
|
|
19
|
+
control), Gross 1998 (emotion regulation strategies).
|
|
20
|
+
|
|
21
|
+
Hard constraints: pure functions, stdlib only, no IO, no main import, time is
|
|
22
|
+
injected (now=), and update() is idempotent for the same now. The core state
|
|
23
|
+
object is never mutated -- callers pass a dict and get a fresh dict back.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import math
|
|
28
|
+
|
|
29
|
+
DIMS = ("tension", "warmth", "heaviness", "clarity")
|
|
30
|
+
|
|
31
|
+
# Set-points / comfort band (Gross 1998 regulation targets; Sterling 2012).
|
|
32
|
+
COMFORT = {"tension": 0.30, "warmth": 0.60, "heaviness": 0.30, "clarity": 0.70}
|
|
33
|
+
BAND = {"tension": 0.15, "warmth": 0.20, "heaviness": 0.15, "clarity": 0.20}
|
|
34
|
+
WEIGHTS = {"tension": 0.30, "warmth": 0.25, "heaviness": 0.25, "clarity": 0.20}
|
|
35
|
+
|
|
36
|
+
# Defaults keep the formulas total and monotone even on partial state dicts.
|
|
37
|
+
DEFAULTS = {
|
|
38
|
+
"stress": 0.0, "arousal": 0.5, "sleep_debt": 0.0, "conflict": 0.0,
|
|
39
|
+
"v": 0.0, "social": 0.5, "trust": 0.5, "closeness": 0.5,
|
|
40
|
+
"sadness": 0.0, "allostatic_load": 0.0, "energy": 0.55,
|
|
41
|
+
"loneliness": 0.0, "noise": 0.0,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _clamp(value, lo=0.0, hi=1.0):
|
|
46
|
+
try:
|
|
47
|
+
return max(lo, min(hi, float(value)))
|
|
48
|
+
except Exception:
|
|
49
|
+
return lo
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _num(source, key, default=0.0):
|
|
53
|
+
if not isinstance(source, dict):
|
|
54
|
+
return float(default)
|
|
55
|
+
try:
|
|
56
|
+
value = source.get(key, default)
|
|
57
|
+
if value is None:
|
|
58
|
+
return float(default)
|
|
59
|
+
return float(value)
|
|
60
|
+
except Exception:
|
|
61
|
+
return float(default)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _noise_value(source):
|
|
65
|
+
"""Noise may be a scalar or the core per-channel noise dict."""
|
|
66
|
+
if not isinstance(source, dict):
|
|
67
|
+
return 0.0
|
|
68
|
+
value = source.get("noise", 0.0)
|
|
69
|
+
if isinstance(value, dict) and value:
|
|
70
|
+
try:
|
|
71
|
+
return _clamp(sum(abs(float(v)) for v in value.values()) / len(value))
|
|
72
|
+
except Exception:
|
|
73
|
+
return 0.0
|
|
74
|
+
return _clamp(value)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _labels(source):
|
|
78
|
+
if not isinstance(source, dict):
|
|
79
|
+
return {}
|
|
80
|
+
labels = source.get("labels")
|
|
81
|
+
return labels if isinstance(labels, dict) else {}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _emotion(source):
|
|
85
|
+
labels = _labels(source)
|
|
86
|
+
return str(labels.get("emotion") or source.get("emotion") or "neutral")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def inputs(state=None):
|
|
90
|
+
"""Extract the formula inputs from a core snapshot / state dict.
|
|
91
|
+
|
|
92
|
+
Accepted keys (all optional): stress, arousal (alias a), sleep_debt,
|
|
93
|
+
conflict, v, social, trust, closeness, sadness, allostatic_load, energy,
|
|
94
|
+
loneliness, noise (scalar or per-channel dict), plus labels.emotion as a
|
|
95
|
+
fallback for sadness/loneliness when those keys are absent.
|
|
96
|
+
"""
|
|
97
|
+
state = state if isinstance(state, dict) else {}
|
|
98
|
+
emotion = _emotion(state)
|
|
99
|
+
arousal = _num(state, "arousal", _num(state, "a", DEFAULTS["arousal"]))
|
|
100
|
+
sadness = _num(state, "sadness", 0.0)
|
|
101
|
+
if not sadness and emotion == "sadness":
|
|
102
|
+
sadness = _clamp(_num(state, "intensity", 0.5), 0.0, 1.0)
|
|
103
|
+
loneliness = _num(state, "loneliness", 0.0)
|
|
104
|
+
if not loneliness and emotion == "loneliness":
|
|
105
|
+
loneliness = _clamp(_num(state, "intensity", 0.5), 0.0, 1.0)
|
|
106
|
+
return {
|
|
107
|
+
"stress": _clamp(_num(state, "stress", 0.0)),
|
|
108
|
+
"arousal": _clamp(arousal),
|
|
109
|
+
"sleep_debt": _clamp(_num(state, "sleep_debt", 0.0)),
|
|
110
|
+
"conflict": _clamp(_num(state, "conflict", 0.0)),
|
|
111
|
+
"v": _clamp(_num(state, "v", 0.0), -1.0, 1.0),
|
|
112
|
+
"social": _clamp(_num(state, "social", 0.5)),
|
|
113
|
+
"trust": _clamp(_num(state, "trust", 0.5)),
|
|
114
|
+
"closeness": _clamp(_num(state, "closeness", 0.5)),
|
|
115
|
+
"sadness": _clamp(sadness),
|
|
116
|
+
"allostatic_load": _clamp(_num(state, "allostatic_load", 0.0)),
|
|
117
|
+
"energy": _clamp(_num(state, "energy", 0.55)),
|
|
118
|
+
"loneliness": _clamp(loneliness),
|
|
119
|
+
"noise": _noise_value(state),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def target(state=None):
|
|
124
|
+
"""The four dimension targets from the body state (no smoothing)."""
|
|
125
|
+
x = inputs(state)
|
|
126
|
+
tension = (0.45 * x["stress"] + 0.25 * x["arousal"]
|
|
127
|
+
+ 0.20 * x["sleep_debt"] + 0.10 * x["conflict"])
|
|
128
|
+
warmth = (0.40 * ((x["v"] + 1.0) / 2.0) + 0.25 * x["social"]
|
|
129
|
+
+ 0.20 * x["trust"] + 0.15 * x["closeness"])
|
|
130
|
+
heaviness = (0.40 * x["sadness"] + 0.30 * x["allostatic_load"]
|
|
131
|
+
+ 0.20 * (1.0 - x["energy"]) + 0.10 * x["loneliness"])
|
|
132
|
+
clarity = (0.40 * (1.0 - x["noise"]) + 0.30 * (1.0 - x["sleep_debt"])
|
|
133
|
+
+ 0.30 * (1.0 - abs(x["arousal"] - 0.5) * 2.0))
|
|
134
|
+
return {"tension": round(_clamp(tension), 4),
|
|
135
|
+
"warmth": round(_clamp(warmth), 4),
|
|
136
|
+
"heaviness": round(_clamp(heaviness), 4),
|
|
137
|
+
"clarity": round(_clamp(clarity), 4)}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _values(intero):
|
|
141
|
+
intero = intero if isinstance(intero, dict) else {}
|
|
142
|
+
return {dim: round(_clamp(intero.get(dim, COMFORT[dim])), 4) for dim in DIMS}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _alpha(dt_hours, rate):
|
|
146
|
+
"""Bounded EMA gain for one step: 1 - exp(-rate * dt)."""
|
|
147
|
+
try:
|
|
148
|
+
dt = max(0.0, float(dt_hours))
|
|
149
|
+
except Exception:
|
|
150
|
+
dt = 0.0
|
|
151
|
+
try:
|
|
152
|
+
r = max(0.0, min(1.0, float(rate)))
|
|
153
|
+
except Exception:
|
|
154
|
+
r = 0.25
|
|
155
|
+
if dt <= 0.0 or r <= 0.0:
|
|
156
|
+
return 0.0
|
|
157
|
+
return 1.0 - math.exp(-r * dt)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def update(prev, state, dt_hours=None, rate=0.25, now=None):
|
|
161
|
+
"""EMA toward target(state); idempotent for the same now.
|
|
162
|
+
|
|
163
|
+
dt_hours is the elapsed time since the previous update. When it is None and
|
|
164
|
+
now plus prev["ts"] are available the delta is derived in hours. A zero
|
|
165
|
+
delta (or an unchanged now) returns the previous values unchanged.
|
|
166
|
+
"""
|
|
167
|
+
prev = prev if isinstance(prev, dict) else {}
|
|
168
|
+
current = _values(prev)
|
|
169
|
+
stamp = None
|
|
170
|
+
if now is not None:
|
|
171
|
+
try:
|
|
172
|
+
stamp = float(now)
|
|
173
|
+
except Exception:
|
|
174
|
+
stamp = None
|
|
175
|
+
if stamp is not None and prev.get("ts") is not None:
|
|
176
|
+
try:
|
|
177
|
+
if abs(float(prev.get("ts")) - stamp) < 1e-9:
|
|
178
|
+
out = dict(current)
|
|
179
|
+
out["ts"] = stamp
|
|
180
|
+
return out
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
if dt_hours is None and stamp is not None and prev.get("ts") is not None:
|
|
184
|
+
try:
|
|
185
|
+
dt_hours = max(0.0, (stamp - float(prev.get("ts"))) / 3600.0)
|
|
186
|
+
except Exception:
|
|
187
|
+
dt_hours = 0.0
|
|
188
|
+
alpha = _alpha(0.0 if dt_hours is None else dt_hours, rate)
|
|
189
|
+
goal = target(state)
|
|
190
|
+
out = {}
|
|
191
|
+
for dim in DIMS:
|
|
192
|
+
out[dim] = round(_clamp(current[dim] + alpha * (goal[dim] - current[dim])), 4)
|
|
193
|
+
if stamp is not None:
|
|
194
|
+
out["ts"] = stamp
|
|
195
|
+
return out
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def discomfort(intero, sp=None):
|
|
199
|
+
"""Band-relative weighted deviation: 0 inside every band, > 0 outside.
|
|
200
|
+
|
|
201
|
+
sp may override set-points via {"comfort": {...}, "band": {...}} or
|
|
202
|
+
directly {dim: value} (set-point override). Returns {total, parts} with
|
|
203
|
+
total normalized to [0, 1] by the worst-case excess.
|
|
204
|
+
"""
|
|
205
|
+
values = _values(intero)
|
|
206
|
+
comfort = dict(COMFORT)
|
|
207
|
+
band = dict(BAND)
|
|
208
|
+
if isinstance(sp, dict):
|
|
209
|
+
if isinstance(sp.get("comfort"), dict):
|
|
210
|
+
comfort.update(sp["comfort"])
|
|
211
|
+
if isinstance(sp.get("band"), dict):
|
|
212
|
+
band.update(sp["band"])
|
|
213
|
+
for dim in DIMS:
|
|
214
|
+
if dim in sp and not isinstance(sp[dim], dict):
|
|
215
|
+
comfort[dim] = sp[dim]
|
|
216
|
+
parts = {}
|
|
217
|
+
raw = 0.0
|
|
218
|
+
worst = 0.0
|
|
219
|
+
for dim in DIMS:
|
|
220
|
+
center = _clamp(comfort.get(dim, COMFORT[dim]))
|
|
221
|
+
half = _clamp(band.get(dim, BAND[dim]), 0.0, 1.0)
|
|
222
|
+
excess = max(0.0, abs(values[dim] - center) - half)
|
|
223
|
+
parts[dim] = round(excess, 4)
|
|
224
|
+
weight = float(WEIGHTS[dim])
|
|
225
|
+
raw += weight * excess
|
|
226
|
+
worst += weight * max(center - half, (1.0 - center) - half, 0.0)
|
|
227
|
+
total = 0.0 if worst <= 0.0 else _clamp(raw / worst)
|
|
228
|
+
return {"total": round(total, 4), "parts": parts}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def view(intero):
|
|
232
|
+
"""Public read model: four dims + scalar discomfort."""
|
|
233
|
+
values = _values(intero)
|
|
234
|
+
values["discomfort"] = discomfort(values)["total"]
|
|
235
|
+
return values
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""P1-2 interoception -> expression style (length / tone / initiative).
|
|
3
|
+
|
|
4
|
+
The four interoceptive channels (tension / warmth / heaviness / clarity, 0..1)
|
|
5
|
+
are felt states, not prompt text. This module is the deterministic bridge that
|
|
6
|
+
turns them into expression knobs the interpretation layer may use:
|
|
7
|
+
|
|
8
|
+
- length: a reply target/cap multiplier (tense, heavy or unclear -> shorter);
|
|
9
|
+
- tone: one short directive for the dominant felt state (or "" when calm);
|
|
10
|
+
- initiative: 0..1 scalar for how forward the next expression should be.
|
|
11
|
+
|
|
12
|
+
Pure functions only: no IO, no main import, no prompt text.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
CHANNELS = ("tension", "warmth", "heaviness", "clarity")
|
|
17
|
+
DEFAULT_INITIATIVE = 0.5
|
|
18
|
+
MIN_TARGET = 4
|
|
19
|
+
MIN_CAP = 8
|
|
20
|
+
MAX_CAP = 120
|
|
21
|
+
TONE_THRESHOLD = 0.55
|
|
22
|
+
|
|
23
|
+
TONE_LINES = {
|
|
24
|
+
"tension": "先接住情绪,句子短一点",
|
|
25
|
+
"heaviness": "慢一点说,别堆太多话",
|
|
26
|
+
"warmth": "语气可以亲近一点",
|
|
27
|
+
"clarity": "没听清就先用一句确认",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _num(value, default=0.0):
|
|
32
|
+
try:
|
|
33
|
+
number = float(value)
|
|
34
|
+
except Exception:
|
|
35
|
+
return float(default)
|
|
36
|
+
if number != number:
|
|
37
|
+
return float(default)
|
|
38
|
+
return number
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _clamp(value, low, high):
|
|
42
|
+
return max(low, min(high, _num(value, 0.0)))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _has_data(intero):
|
|
46
|
+
return isinstance(intero, dict) and any(name in intero for name in CHANNELS)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def levels(intero):
|
|
50
|
+
"""Clamped 0..1 intero channel dict (unknown channels = 0)."""
|
|
51
|
+
data = intero if isinstance(intero, dict) else {}
|
|
52
|
+
return {name: round(_clamp(data.get(name, 0.0), 0.0, 1.0), 4)
|
|
53
|
+
for name in CHANNELS}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def tone(intero):
|
|
57
|
+
"""Dominant felt-state directive, "" when nothing stands out.
|
|
58
|
+
|
|
59
|
+
Low clarity is a special case: not hearing the other side well is a
|
|
60
|
+
directive of its own (ask one clarifying question) even when it is not the
|
|
61
|
+
numerically highest channel.
|
|
62
|
+
"""
|
|
63
|
+
if not _has_data(intero):
|
|
64
|
+
return ""
|
|
65
|
+
felt = levels(intero)
|
|
66
|
+
if felt["clarity"] < TONE_THRESHOLD * 0.5:
|
|
67
|
+
return TONE_LINES["clarity"]
|
|
68
|
+
ranked = sorted(felt.items(), key=lambda item: item[1], reverse=True)
|
|
69
|
+
name, value = ranked[0]
|
|
70
|
+
if value < TONE_THRESHOLD:
|
|
71
|
+
return ""
|
|
72
|
+
if name == "clarity":
|
|
73
|
+
return ""
|
|
74
|
+
return TONE_LINES.get(name, "")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def length_factor(intero):
|
|
78
|
+
"""Multiplier for reply length: warmth lengthens, tension/heaviness shortens."""
|
|
79
|
+
if not _has_data(intero):
|
|
80
|
+
return 1.0
|
|
81
|
+
felt = levels(intero)
|
|
82
|
+
factor = (1.0
|
|
83
|
+
- 0.50 * felt["tension"]
|
|
84
|
+
- 0.40 * felt["heaviness"]
|
|
85
|
+
+ 0.40 * felt["warmth"]
|
|
86
|
+
+ 0.25 * (felt["clarity"] - 0.5))
|
|
87
|
+
return round(_clamp(factor, 0.4, 1.6), 3)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def initiative(intero):
|
|
91
|
+
"""How forward the next expression should be (0..1)."""
|
|
92
|
+
if not _has_data(intero):
|
|
93
|
+
return DEFAULT_INITIATIVE
|
|
94
|
+
felt = levels(intero)
|
|
95
|
+
value = (DEFAULT_INITIATIVE
|
|
96
|
+
+ 0.45 * felt["warmth"]
|
|
97
|
+
- 0.35 * felt["heaviness"]
|
|
98
|
+
- 0.30 * felt["tension"]
|
|
99
|
+
+ 0.10 * felt["clarity"])
|
|
100
|
+
return round(_clamp(value, 0.0, 1.0), 3)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def style(intero, target=10, cap=30):
|
|
104
|
+
"""Reply knobs for one interoceptive state (bounded, deterministic)."""
|
|
105
|
+
base_target = max(MIN_TARGET, int(_num(target, 10)))
|
|
106
|
+
base_cap = max(MIN_CAP, int(_num(cap, 30)))
|
|
107
|
+
factor = length_factor(intero)
|
|
108
|
+
new_target = max(MIN_TARGET, int(round(base_target * factor)))
|
|
109
|
+
new_cap = max(new_target, max(MIN_CAP, int(round(base_cap * factor))))
|
|
110
|
+
new_cap = min(MAX_CAP, new_cap)
|
|
111
|
+
directive = tone(intero)
|
|
112
|
+
return {"target": new_target, "cap": new_cap,
|
|
113
|
+
"tone": directive, "initiative": initiative(intero),
|
|
114
|
+
"factor": factor,
|
|
115
|
+
"applied": bool(directive) or abs(factor - 1.0) > 1e-9,
|
|
116
|
+
"levels": levels(intero)}
|