dsh-biocore 0.2.0 → 0.3.0
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 +11 -0
- package/lib/core-worker.js +268 -0
- package/lib/index.js +160 -70
- package/lib/modules/autonomy.js +125 -0
- package/lib/modules/cache-stats.js +48 -0
- package/lib/modules/core-turn.js +24 -6
- package/lib/modules/expression-inject.js +0 -10
- package/lib/modules/expression-refresh.js +0 -9
- package/lib/modules/memory-capture.js +2 -1
- package/lib/modules/memory-inject.js +50 -5
- package/lib/modules/memory-tools.js +7 -4
- package/lib/modules/python-env.js +46 -0
- package/lib/modules/safety-gate.js +0 -3
- package/lib/modules/series.js +54 -0
- package/lib/modules/switch.js +16 -0
- package/lib/py-bridge.js +81 -15
- package/package.json +1 -1
- package/python/biocore_memory/core/config.py +138 -127
- package/python/biocore_memory/engine.py +2653 -2476
- package/python/biocore_memory/recall/embedding.py +129 -0
- package/python/biocore_memory/recall/feedback.py +202 -0
- package/python/biocore_model/preference_drift.py +255 -0
- package/python/biocore_model/tests/__init__.py +1 -0
- package/python/biocore_model/tests/core_mode_ui_check.mjs +63 -0
- package/python/biocore_model/tests/core_status_check.mjs +29 -0
- package/python/biocore_model/tests/dom_safe_check.mjs +22 -0
- package/python/biocore_model/tests/leak_samples_run.py +18 -0
- package/python/biocore_model/tests/orb_ring_check.mjs +20 -0
- package/python/biocore_model/tests/panel_html_check.mjs +63 -0
- package/python/biocore_model/tests/persona_flow_check.mjs +17 -0
- package/python/biocore_model/tests/render_guard_check.mjs +19 -0
- package/python/biocore_model/tests/switch_groups_check.mjs +94 -0
- package/python/biocore_model/tests/tabs_state_check.mjs +24 -0
- package/python/biocore_model/tests/test_affective_labor.py +102 -0
- package/python/biocore_model/tests/test_appraisal.py +137 -0
- package/python/biocore_model/tests/test_appraisal_action.py +55 -0
- package/python/biocore_model/tests/test_arch_convergence.py +35 -0
- package/python/biocore_model/tests/test_attachment_behavior.py +95 -0
- package/python/biocore_model/tests/test_attachment_regulation.py +63 -0
- package/python/biocore_model/tests/test_attachment_state.py +29 -0
- package/python/biocore_model/tests/test_attribution.py +56 -0
- package/python/biocore_model/tests/test_attributor.py +108 -0
- package/python/biocore_model/tests/test_autonomic.py +69 -0
- package/python/biocore_model/tests/test_autonomy_accumulator.py +84 -0
- package/python/biocore_model/tests/test_autonomy_affect.py +62 -0
- package/python/biocore_model/tests/test_autonomy_blame.py +44 -0
- package/python/biocore_model/tests/test_autonomy_caps.py +29 -0
- package/python/biocore_model/tests/test_autonomy_check.py +40 -0
- package/python/biocore_model/tests/test_autonomy_clingy.py +41 -0
- package/python/biocore_model/tests/test_autonomy_cooldown_selfcontact.py +138 -0
- package/python/biocore_model/tests/test_autonomy_frustration.py +42 -0
- package/python/biocore_model/tests/test_autonomy_hourly_limit.py +46 -0
- package/python/biocore_model/tests/test_autonomy_loop.py +64 -0
- package/python/biocore_model/tests/test_autonomy_mode.py +37 -0
- package/python/biocore_model/tests/test_autonomy_model.py +60 -0
- package/python/biocore_model/tests/test_autonomy_model_v2.py +68 -0
- package/python/biocore_model/tests/test_autonomy_module_text.py +104 -0
- package/python/biocore_model/tests/test_autonomy_params.py +163 -0
- package/python/biocore_model/tests/test_autonomy_reason.py +34 -0
- package/python/biocore_model/tests/test_autonomy_self.py +153 -0
- package/python/biocore_model/tests/test_autonomy_send_gate.py +38 -0
- package/python/biocore_model/tests/test_autonomy_silence.py +37 -0
- package/python/biocore_model/tests/test_autonomy_status.py +35 -0
- package/python/biocore_model/tests/test_autonomy_test_trigger.py +24 -0
- package/python/biocore_model/tests/test_autonomy_tick.py +148 -0
- package/python/biocore_model/tests/test_autonomy_v4.py +64 -0
- package/python/biocore_model/tests/test_autonomy_v5.py +43 -0
- package/python/biocore_model/tests/test_autonomy_v7.py +48 -0
- package/python/biocore_model/tests/test_autonomy_v_theta.py +85 -0
- package/python/biocore_model/tests/test_batch1_wiring.py +231 -0
- package/python/biocore_model/tests/test_batch2_wiring.py +231 -0
- package/python/biocore_model/tests/test_batch3_wiring.py +223 -0
- package/python/biocore_model/tests/test_bio_tick.py +202 -0
- package/python/biocore_model/tests/test_boundary.py +183 -0
- package/python/biocore_model/tests/test_boundary_gate.py +295 -0
- package/python/biocore_model/tests/test_brain_split.py +99 -0
- package/python/biocore_model/tests/test_co_recall.py +350 -0
- package/python/biocore_model/tests/test_commitment_extractor.py +197 -0
- package/python/biocore_model/tests/test_compassion_fatigue.py +84 -0
- package/python/biocore_model/tests/test_connection_bids.py +69 -0
- package/python/biocore_model/tests/test_core_bridge.py +88 -0
- package/python/biocore_model/tests/test_core_bridge_episode.py +14 -0
- package/python/biocore_model/tests/test_core_bridge_forget.py +17 -0
- package/python/biocore_model/tests/test_core_bridge_identity.py +15 -0
- package/python/biocore_model/tests/test_core_bridge_media.py +17 -0
- package/python/biocore_model/tests/test_core_bridge_relation.py +17 -0
- package/python/biocore_model/tests/test_core_bridge_summary.py +17 -0
- package/python/biocore_model/tests/test_core_client.py +156 -0
- package/python/biocore_model/tests/test_core_commands.py +146 -0
- package/python/biocore_model/tests/test_core_mode.py +161 -0
- package/python/biocore_model/tests/test_core_state.py +240 -0
- package/python/biocore_model/tests/test_criticality.py +61 -0
- package/python/biocore_model/tests/test_cross_session_copy.py +67 -0
- package/python/biocore_model/tests/test_debug_values.py +169 -0
- package/python/biocore_model/tests/test_defense_mechanisms.py +177 -0
- package/python/biocore_model/tests/test_delta_budget.py +157 -0
- package/python/biocore_model/tests/test_diag_trace.py +201 -0
- package/python/biocore_model/tests/test_directives_compat.py +698 -0
- package/python/biocore_model/tests/test_dissonance.py +97 -0
- package/python/biocore_model/tests/test_drift_sign.py +87 -0
- package/python/biocore_model/tests/test_embedding_fallback.py +277 -0
- package/python/biocore_model/tests/test_embedding_local.py +279 -0
- package/python/biocore_model/tests/test_embedding_mode.py +175 -0
- package/python/biocore_model/tests/test_embedding_modes.py +342 -0
- package/python/biocore_model/tests/test_emotion_geometry.py +114 -0
- package/python/biocore_model/tests/test_emotion_probe.py +214 -0
- package/python/biocore_model/tests/test_emotion_probe_concurrency.py +128 -0
- package/python/biocore_model/tests/test_entity_graph.py +198 -0
- package/python/biocore_model/tests/test_entity_resolve.py +446 -0
- package/python/biocore_model/tests/test_envy.py +173 -0
- package/python/biocore_model/tests/test_exit_or_silence.py +209 -0
- package/python/biocore_model/tests/test_expr_guard.py +1 -0
- package/python/biocore_model/tests/test_expr_intent.py +66 -0
- package/python/biocore_model/tests/test_expr_state.py +66 -0
- package/python/biocore_model/tests/test_expr_template.py +71 -0
- package/python/biocore_model/tests/test_expr_tone.py +41 -0
- package/python/biocore_model/tests/test_extreme_scenarios.py +147 -0
- package/python/biocore_model/tests/test_feedback_flow.py +185 -0
- package/python/biocore_model/tests/test_feedback_guard.py +65 -0
- package/python/biocore_model/tests/test_feedback_llm.py +59 -0
- package/python/biocore_model/tests/test_feedback_llm_inferred.py +140 -0
- package/python/biocore_model/tests/test_feedback_memory.py +63 -0
- package/python/biocore_model/tests/test_feedback_user.py +61 -0
- package/python/biocore_model/tests/test_frustration_curve.py +66 -0
- package/python/biocore_model/tests/test_global_bio_state.py +116 -0
- package/python/biocore_model/tests/test_governance.py +181 -0
- package/python/biocore_model/tests/test_granularity.py +133 -0
- package/python/biocore_model/tests/test_graph_extract.py +224 -0
- package/python/biocore_model/tests/test_graph_recall.py +226 -0
- package/python/biocore_model/tests/test_graph_sqlite.py +298 -0
- package/python/biocore_model/tests/test_growth.py +301 -0
- package/python/biocore_model/tests/test_intent_render.py +60 -0
- package/python/biocore_model/tests/test_intero_regulation.py +224 -0
- package/python/biocore_model/tests/test_intero_style.py +146 -0
- package/python/biocore_model/tests/test_item3_numeric.py +105 -0
- package/python/biocore_model/tests/test_item6_numeric.py +116 -0
- package/python/biocore_model/tests/test_layered_memory.py +174 -0
- package/python/biocore_model/tests/test_leak_guard.py +75 -0
- package/python/biocore_model/tests/test_learning_probe.py +93 -0
- package/python/biocore_model/tests/test_life_loop.py +222 -0
- package/python/biocore_model/tests/test_life_loop_10s.py +131 -0
- package/python/biocore_model/tests/test_local_embedding_health.py +258 -0
- package/python/biocore_model/tests/test_memory_affect_encoding.py +79 -0
- package/python/biocore_model/tests/test_memory_backend.py +332 -0
- package/python/biocore_model/tests/test_memory_dedup.py +316 -0
- package/python/biocore_model/tests/test_memory_dual.py +219 -0
- package/python/biocore_model/tests/test_memory_dual_wiring.py +219 -0
- package/python/biocore_model/tests/test_memory_feedback.py +241 -0
- package/python/biocore_model/tests/test_memory_fixes.py +84 -0
- package/python/biocore_model/tests/test_memory_forgetting.py +58 -0
- package/python/biocore_model/tests/test_memory_inject.py +211 -0
- package/python/biocore_model/tests/test_memory_mood_recall.py +118 -0
- package/python/biocore_model/tests/test_memory_store.py +372 -0
- package/python/biocore_model/tests/test_metacog.py +103 -0
- package/python/biocore_model/tests/test_migrate.py +245 -0
- package/python/biocore_model/tests/test_migrate_store.py +194 -0
- package/python/biocore_model/tests/test_mind_wander.py +65 -0
- package/python/biocore_model/tests/test_mood.py +35 -0
- package/python/biocore_model/tests/test_narrative.py +114 -0
- package/python/biocore_model/tests/test_needs_esteem.py +51 -0
- package/python/biocore_model/tests/test_needs_events.py +205 -0
- package/python/biocore_model/tests/test_needs_sleep.py +54 -0
- package/python/biocore_model/tests/test_noise_bank.py +54 -0
- package/python/biocore_model/tests/test_numeric_schema.py +98 -0
- package/python/biocore_model/tests/test_output_protocol.py +65 -0
- package/python/biocore_model/tests/test_page_debug.py +147 -0
- package/python/biocore_model/tests/test_page_status_autonomy.py +215 -0
- package/python/biocore_model/tests/test_pipeline.py +345 -0
- package/python/biocore_model/tests/test_pipeline_api.py +192 -0
- package/python/biocore_model/tests/test_pipeline_mode.py +222 -0
- package/python/biocore_model/tests/test_plugin_config.py +85 -0
- package/python/biocore_model/tests/test_policy_learning.py +116 -0
- package/python/biocore_model/tests/test_possible_selves.py +88 -0
- package/python/biocore_model/tests/test_predictor.py +227 -0
- package/python/biocore_model/tests/test_preference_drift.py +142 -0
- package/python/biocore_model/tests/test_preset.py +142 -0
- package/python/biocore_model/tests/test_proactive_context.py +70 -0
- package/python/biocore_model/tests/test_proactive_history.py +90 -0
- package/python/biocore_model/tests/test_proactive_memory.py +141 -0
- package/python/biocore_model/tests/test_proactive_send_record.py +88 -0
- package/python/biocore_model/tests/test_probe_contract.py +71 -0
- package/python/biocore_model/tests/test_prospective.py +154 -0
- package/python/biocore_model/tests/test_provider_fallback.py +89 -0
- package/python/biocore_model/tests/test_reconstruction.py +111 -0
- package/python/biocore_model/tests/test_regulation_strategies.py +228 -0
- package/python/biocore_model/tests/test_relation_stage.py +114 -0
- package/python/biocore_model/tests/test_relationship_stage.py +59 -0
- package/python/biocore_model/tests/test_release_140.py +55 -0
- package/python/biocore_model/tests/test_repair_after_boundary.py +249 -0
- package/python/biocore_model/tests/test_reply_limit.py +86 -0
- package/python/biocore_model/tests/test_reply_limit_persist.py +48 -0
- package/python/biocore_model/tests/test_reply_split.py +52 -0
- package/python/biocore_model/tests/test_reply_split_astrbot.py +66 -0
- package/python/biocore_model/tests/test_reply_split_inject.py +53 -0
- package/python/biocore_model/tests/test_rerank.py +229 -0
- package/python/biocore_model/tests/test_rollback.py +175 -0
- package/python/biocore_model/tests/test_runtime.py +175 -0
- package/python/biocore_model/tests/test_runtime_active.py +205 -0
- package/python/biocore_model/tests/test_runtime_pipeline.py +1583 -0
- package/python/biocore_model/tests/test_runtime_proactive.py +231 -0
- package/python/biocore_model/tests/test_scheduler.py +79 -0
- package/python/biocore_model/tests/test_secrecy.py +105 -0
- package/python/biocore_model/tests/test_self_esteem.py +45 -0
- package/python/biocore_model/tests/test_self_intent.py +347 -0
- package/python/biocore_model/tests/test_self_interest.py +183 -0
- package/python/biocore_model/tests/test_self_layer_wiring.py +198 -0
- package/python/biocore_model/tests/test_self_verification.py +113 -0
- package/python/biocore_model/tests/test_situation.py +142 -0
- package/python/biocore_model/tests/test_sleep_circadian.py +41 -0
- package/python/biocore_model/tests/test_social_pain.py +49 -0
- package/python/biocore_model/tests/test_social_repair.py +67 -0
- package/python/biocore_model/tests/test_social_theory.py +78 -0
- package/python/biocore_model/tests/test_source_monitoring.py +204 -0
- package/python/biocore_model/tests/test_speak_like_group.py +36 -0
- package/python/biocore_model/tests/test_stages.py +226 -0
- package/python/biocore_model/tests/test_state_tag.py +86 -0
- package/python/biocore_model/tests/test_template_expr.py +65 -0
- package/python/biocore_model/tests/test_text_inference.py +70 -0
- package/python/biocore_model/tests/test_theta_params.py +115 -0
- package/python/biocore_model/tests/test_topic_control.py +200 -0
- package/python/biocore_model/tests/test_turn_memory.py +105 -0
- package/python/biocore_model/tests/test_turn_memory_panel.py +174 -0
- package/python/biocore_model/tests/test_ui_settings.py +84 -0
- package/python/biocore_model/tests/test_ui_settings_authority.py +179 -0
- package/python/biocore_model/tests/test_user_appraisal_async.py +155 -0
- package/python/biocore_model/tests/test_user_classify.py +122 -0
- package/python/biocore_model/tests/test_value_order.py +117 -0
- package/python/biocore_model/tests/test_voice_bank.py +105 -0
- package/python/biocore_model/tests/test_waiting_sense.py +216 -0
- package/python/biocore_model/tests/test_x_directives.py +101 -0
- package/python/biocore_model/tests/ui_compat_check.mjs +22 -0
- package/python/biocore_model/tests/view_mode_check.mjs +17 -0
- package/python/biocore_runtime/autonomy.py +208 -0
- package/python/biocore_runtime/core_state.py +302 -0
- package/python/biocore_runtime/governance.py +495 -0
- package/python/biocore_runtime/growth.py +528 -0
- package/python/biocore_runtime/pipeline.py +850 -23
- package/python/biocore_runtime/relationship_state.py +214 -0
- package/python/biocore_runtime/self_layer.py +103 -0
- package/python/biocore_runtime/service.py +162 -0
- package/python/biocore_runtime/standby.py +472 -0
- package/ui/panel-layout.js +83 -39
- package/ui/panel.css +885 -743
- package/ui/panel.html +356 -296
- package/ui/panel.js +739 -865
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""task-220: growth metrics + a read-only consolidation dry-run.
|
|
3
|
+
|
|
4
|
+
Two read-only products over the DSH runtime store (<store>/sessions/...,
|
|
5
|
+
traces/, relationship.json):
|
|
6
|
+
|
|
7
|
+
1. metrics(store) -> one JSON object:
|
|
8
|
+
memory total / added_this_week / avg_importance / with_affect ratio
|
|
9
|
+
recall hits_total / mood_match_hits / avg_top_score
|
|
10
|
+
relation trust, closeness, change_7d (from growth_history.jsonl)
|
|
11
|
+
narrative intent distribution (top 3, from turn traces)
|
|
12
|
+
flags the four learning switches and where their value came from
|
|
13
|
+
|
|
14
|
+
2. consolidate(store) -> candidate facts from the newest N memory entries
|
|
15
|
+
(the only per-turn record that carries text):
|
|
16
|
+
repeated topics (>= TOPIC_MIN_REPEAT episodes, reusing
|
|
17
|
+
biocore_model.preference_drift.topics)
|
|
18
|
+
stable relationship facts (trust/closeness above RELATION_STABLE_MIN)
|
|
19
|
+
The result is written to <store>/consolidate_report.json. It never calls
|
|
20
|
+
save_memory()/tool_call(), never edits an entry and never calls a model:
|
|
21
|
+
dry_run is always true and memory_written always false.
|
|
22
|
+
|
|
23
|
+
Both are OFF by default: {"growth_report": True} or
|
|
24
|
+
BIOCORE_GROWTH_REPORT=1 enables consolidate(); metrics() is pure read-only and
|
|
25
|
+
can be called without the switch (the switch is reported in the output).
|
|
26
|
+
|
|
27
|
+
Pure-ish: no network, no LLM, no host imports. The only writes are the report
|
|
28
|
+
file and one history line per day.
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import math
|
|
34
|
+
import time
|
|
35
|
+
from collections import Counter
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
from . import pipeline as _pipeline
|
|
40
|
+
except Exception: # pragma: no cover - script mode
|
|
41
|
+
from biocore_runtime import pipeline as _pipeline
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
from biocore_model import preference_drift as _drift
|
|
45
|
+
except Exception: # pragma: no cover - degraded checkout
|
|
46
|
+
_drift = None
|
|
47
|
+
|
|
48
|
+
LEARNING_FLAGS = ("mood_recall", "affect_encoding",
|
|
49
|
+
"memory_feedback", "preference_drift")
|
|
50
|
+
ENV_FLAGS = {
|
|
51
|
+
"mood_recall": "BIOCORE_MOOD_RECALL",
|
|
52
|
+
"affect_encoding": "BIOCORE_AFFECT_ENCODING",
|
|
53
|
+
"memory_feedback": "BIOCORE_MEMORY_FEEDBACK",
|
|
54
|
+
"preference_drift": "BIOCORE_PREFERENCE_DRIFT",
|
|
55
|
+
}
|
|
56
|
+
GROWTH_ENV = "BIOCORE_GROWTH_REPORT"
|
|
57
|
+
REPORT_FILE = "consolidate_report.json"
|
|
58
|
+
HISTORY_FILE = "growth_history.jsonl"
|
|
59
|
+
DEFAULT_EPISODES = 30
|
|
60
|
+
HISTORY_KEEP = 120
|
|
61
|
+
WEEK_SECONDS = 7 * 86400.0
|
|
62
|
+
SECTION_TOP = 3
|
|
63
|
+
TOPIC_MIN_REPEAT = 2
|
|
64
|
+
MAX_CANDIDATES = 8
|
|
65
|
+
RELATION_STABLE_MIN = 0.60
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _num(value, default=0.0):
|
|
69
|
+
try:
|
|
70
|
+
number = float(value)
|
|
71
|
+
except Exception:
|
|
72
|
+
return default
|
|
73
|
+
return number if number == number else default
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _root(store):
|
|
77
|
+
if isinstance(store, dict):
|
|
78
|
+
raw = store.get("root") or store.get("store") or store.get("path")
|
|
79
|
+
else:
|
|
80
|
+
raw = store
|
|
81
|
+
text = str(raw or "").strip()
|
|
82
|
+
return Path(text) if text else None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _read_json(path, fallback):
|
|
86
|
+
try:
|
|
87
|
+
if path is None or not Path(path).exists():
|
|
88
|
+
return fallback
|
|
89
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
90
|
+
return data if isinstance(data, dict) else fallback
|
|
91
|
+
except Exception:
|
|
92
|
+
return fallback
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _atomic_write(path, data):
|
|
96
|
+
path = Path(path)
|
|
97
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
tmp = path.with_name(path.name + ".tmp")
|
|
99
|
+
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2),
|
|
100
|
+
encoding="utf-8")
|
|
101
|
+
tmp.replace(path)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _truthy(raw):
|
|
105
|
+
return str(raw or "").strip().lower() in ("1", "true", "on", "yes")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def enabled(flags=None, env=None):
|
|
109
|
+
"""Flag or env switch; both default OFF."""
|
|
110
|
+
data = flags if isinstance(flags, dict) else {}
|
|
111
|
+
if "growth_report" in data:
|
|
112
|
+
return bool(data.get("growth_report"))
|
|
113
|
+
raw = env.get(GROWTH_ENV) if hasattr(env, "get") else None
|
|
114
|
+
return _truthy(raw)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# consolidate() takes an "enabled" argument, so keep a second name for the
|
|
118
|
+
# function itself (the parameter must not shadow the module's switch reader).
|
|
119
|
+
_enabled = enabled
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def report_path(store):
|
|
123
|
+
root = _root(store)
|
|
124
|
+
return (root / REPORT_FILE) if root is not None else None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def history_path(store):
|
|
128
|
+
root = _root(store)
|
|
129
|
+
return (root / HISTORY_FILE) if root is not None else None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---------------------------------------------------------------- readers
|
|
133
|
+
def session_ids(store):
|
|
134
|
+
root = _root(store)
|
|
135
|
+
base = (root / "sessions") if root is not None else None
|
|
136
|
+
if base is None or not base.exists():
|
|
137
|
+
return []
|
|
138
|
+
try:
|
|
139
|
+
return sorted(child.name for child in base.iterdir()
|
|
140
|
+
if child.is_dir())
|
|
141
|
+
except Exception:
|
|
142
|
+
return []
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def entries(store):
|
|
146
|
+
"""Every visible memory entry, newest first, tagged with its session."""
|
|
147
|
+
rows = []
|
|
148
|
+
root = _root(store)
|
|
149
|
+
base = (root / "sessions") if root is not None else None
|
|
150
|
+
if base is None or not base.exists():
|
|
151
|
+
return rows
|
|
152
|
+
try:
|
|
153
|
+
sessions = sorted(child for child in base.iterdir() if child.is_dir())
|
|
154
|
+
except Exception:
|
|
155
|
+
return rows
|
|
156
|
+
for directory in sessions:
|
|
157
|
+
doc = _read_json(directory / "memory.json", {})
|
|
158
|
+
for entry in doc.get("entries") or []:
|
|
159
|
+
if not isinstance(entry, dict):
|
|
160
|
+
continue
|
|
161
|
+
if entry.get("deleted") or entry.get("archived"):
|
|
162
|
+
continue
|
|
163
|
+
item = dict(entry)
|
|
164
|
+
item["_session"] = directory.name
|
|
165
|
+
rows.append(item)
|
|
166
|
+
rows.sort(key=lambda row: _num(row.get("ts"), 0.0), reverse=True)
|
|
167
|
+
return rows
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def episodes(store, limit=DEFAULT_EPISODES):
|
|
171
|
+
"""Newest N memory entries: the per-turn record that carries text."""
|
|
172
|
+
count = max(1, int(_num(limit, DEFAULT_EPISODES)))
|
|
173
|
+
return entries(store)[:count]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def traces(store):
|
|
177
|
+
rows = []
|
|
178
|
+
for session_id in session_ids(store):
|
|
179
|
+
try:
|
|
180
|
+
rows.extend(_pipeline.trace_read(store, session_id,
|
|
181
|
+
limit=_pipeline.TRACE_KEEP))
|
|
182
|
+
except Exception:
|
|
183
|
+
continue
|
|
184
|
+
rows.sort(key=lambda row: _num(row.get("ts"), 0.0))
|
|
185
|
+
return rows
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def relationship(store):
|
|
189
|
+
try:
|
|
190
|
+
return _pipeline.relationship_read(store)["relationship"]
|
|
191
|
+
except Exception:
|
|
192
|
+
return {"trust": 0.5, "closeness": 0.5, "conflict": 0.0,
|
|
193
|
+
"attachment_style": "secure", "bids": {},
|
|
194
|
+
"days_known": 0}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def read_history(store):
|
|
198
|
+
path = history_path(store)
|
|
199
|
+
rows = []
|
|
200
|
+
try:
|
|
201
|
+
if path is None or not path.exists():
|
|
202
|
+
return rows
|
|
203
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
204
|
+
line = line.strip()
|
|
205
|
+
if not line:
|
|
206
|
+
continue
|
|
207
|
+
try:
|
|
208
|
+
row = json.loads(line)
|
|
209
|
+
except Exception:
|
|
210
|
+
continue
|
|
211
|
+
if isinstance(row, dict):
|
|
212
|
+
rows.append(row)
|
|
213
|
+
except Exception:
|
|
214
|
+
return []
|
|
215
|
+
rows.sort(key=lambda row: _num(row.get("ts"), 0.0))
|
|
216
|
+
return rows
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _record_history(store, now, current):
|
|
220
|
+
path = history_path(store)
|
|
221
|
+
if path is None:
|
|
222
|
+
return False
|
|
223
|
+
rows = read_history(store)
|
|
224
|
+
day = time.strftime("%Y-%m-%d", time.gmtime(_num(now, time.time())))
|
|
225
|
+
last = rows[-1] if rows else {}
|
|
226
|
+
last_day = time.strftime("%Y-%m-%d", time.gmtime(_num(last.get("ts"), 0)))
|
|
227
|
+
if last and last_day == day:
|
|
228
|
+
return False
|
|
229
|
+
rows.append({"ts": round(_num(now, time.time()), 3),
|
|
230
|
+
"trust": round(_num(current.get("trust"), 0.5), 4),
|
|
231
|
+
"closeness": round(_num(current.get("closeness"), 0.5), 4)})
|
|
232
|
+
rows = rows[-HISTORY_KEEP:]
|
|
233
|
+
try:
|
|
234
|
+
_atomic_write_text(path, "\n".join(
|
|
235
|
+
json.dumps(row, ensure_ascii=False, separators=(",", ":"))
|
|
236
|
+
for row in rows) + "\n")
|
|
237
|
+
return True
|
|
238
|
+
except Exception:
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _atomic_write_text(path, text):
|
|
243
|
+
path = Path(path)
|
|
244
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
245
|
+
tmp = path.with_name(path.name + ".tmp")
|
|
246
|
+
tmp.write_text(text, encoding="utf-8")
|
|
247
|
+
tmp.replace(path)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _session_affect(store, session_id):
|
|
251
|
+
directory = _pipeline.session_dir(store, session_id)
|
|
252
|
+
if directory is None:
|
|
253
|
+
return None
|
|
254
|
+
state = _read_json(directory / "state.json", {})
|
|
255
|
+
core = state.get("core") if isinstance(state.get("core"), dict) else {}
|
|
256
|
+
for source in (state, core):
|
|
257
|
+
valence = source.get("v", source.get("valence"))
|
|
258
|
+
if valence is None:
|
|
259
|
+
continue
|
|
260
|
+
arousal = source.get("a", source.get("arousal", 0.5))
|
|
261
|
+
return (max(-1.0, min(1.0, _num(valence, 0.0))),
|
|
262
|
+
max(0.0, min(1.0, _num(arousal, 0.5))))
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _entry_affect(entry):
|
|
267
|
+
affect = entry.get("affect") if isinstance(entry, dict) else None
|
|
268
|
+
if not isinstance(affect, dict):
|
|
269
|
+
return None
|
|
270
|
+
valence = affect.get("valence", affect.get("v"))
|
|
271
|
+
arousal = affect.get("arousal", affect.get("a", 0.5))
|
|
272
|
+
if valence is None:
|
|
273
|
+
return None
|
|
274
|
+
try:
|
|
275
|
+
return (max(-1.0, min(1.0, float(valence))),
|
|
276
|
+
max(0.0, min(1.0, float(arousal))))
|
|
277
|
+
except Exception:
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _cosine(left, right):
|
|
282
|
+
lv, la = left
|
|
283
|
+
rv, ra = right
|
|
284
|
+
ln = math.sqrt(lv * lv + la * la)
|
|
285
|
+
rn = math.sqrt(rv * rv + ra * ra)
|
|
286
|
+
if not ln or not rn:
|
|
287
|
+
return 0.0
|
|
288
|
+
return (lv * rv + la * ra) / (ln * rn)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _flags(store, flags=None, env=None):
|
|
292
|
+
data = flags if isinstance(flags, dict) else {}
|
|
293
|
+
environ = env if hasattr(env, "get") else {}
|
|
294
|
+
root = _root(store)
|
|
295
|
+
file_flags = {}
|
|
296
|
+
if root is not None:
|
|
297
|
+
# DSH keeps flags.json in the data dir above the memory store, so the
|
|
298
|
+
# store root is checked first and its parent second.
|
|
299
|
+
for path in (root / "flags.json", root.parent / "flags.json"):
|
|
300
|
+
if path.exists():
|
|
301
|
+
file_flags = _read_json(path, {})
|
|
302
|
+
break
|
|
303
|
+
out = {}
|
|
304
|
+
for key in LEARNING_FLAGS:
|
|
305
|
+
if key in data:
|
|
306
|
+
out[key] = {"enabled": bool(data[key]), "source": "flags"}
|
|
307
|
+
elif key in file_flags:
|
|
308
|
+
out[key] = {"enabled": bool(file_flags[key]), "source": "flags.json"}
|
|
309
|
+
elif ENV_FLAGS.get(key) and ENV_FLAGS[key] in environ:
|
|
310
|
+
out[key] = {"enabled": _truthy(environ.get(ENV_FLAGS[key])),
|
|
311
|
+
"source": "env"}
|
|
312
|
+
else:
|
|
313
|
+
out[key] = {"enabled": False, "source": "default"}
|
|
314
|
+
return out
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# ---------------------------------------------------------------- metrics
|
|
318
|
+
def _change_7d(store, now, current):
|
|
319
|
+
rows = read_history(store)
|
|
320
|
+
if not rows:
|
|
321
|
+
return {"trust": 0.0, "closeness": 0.0}, "no_history", 0.0
|
|
322
|
+
target = _num(now, time.time()) - WEEK_SECONDS
|
|
323
|
+
older = [row for row in rows if _num(row.get("ts"), 0.0) <= target]
|
|
324
|
+
if older:
|
|
325
|
+
base = older[-1]
|
|
326
|
+
basis = "seven_day"
|
|
327
|
+
else:
|
|
328
|
+
base = rows[0]
|
|
329
|
+
basis = "oldest_sample"
|
|
330
|
+
return (
|
|
331
|
+
{"trust": round(_num(current.get("trust"), 0.5)
|
|
332
|
+
- _num(base.get("trust"), 0.5), 4),
|
|
333
|
+
"closeness": round(_num(current.get("closeness"), 0.5)
|
|
334
|
+
- _num(base.get("closeness"), 0.5), 4)},
|
|
335
|
+
basis,
|
|
336
|
+
round(_num(base.get("ts"), 0.0), 3),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def metrics(store, now=None, flags=None, env=None):
|
|
341
|
+
"""Observable growth metrics; never writes anything."""
|
|
342
|
+
stamp = _num(now, time.time())
|
|
343
|
+
rows = entries(store)
|
|
344
|
+
total = len(rows)
|
|
345
|
+
week = stamp - WEEK_SECONDS
|
|
346
|
+
added = 0
|
|
347
|
+
importance = []
|
|
348
|
+
with_affect = 0
|
|
349
|
+
for entry in rows:
|
|
350
|
+
if _num(entry.get("ts"), 0.0) >= week:
|
|
351
|
+
added += 1
|
|
352
|
+
importance.append(_num(entry.get("importance"), 0.0))
|
|
353
|
+
if _entry_affect(entry) is not None:
|
|
354
|
+
with_affect += 1
|
|
355
|
+
avg_importance = (round(sum(importance) / total, 4) if total else 0.0)
|
|
356
|
+
|
|
357
|
+
by_id = {str(entry.get("id") or ""): entry for entry in rows}
|
|
358
|
+
trace_rows = traces(store)
|
|
359
|
+
hits_total = 0
|
|
360
|
+
mood_match_hits = 0
|
|
361
|
+
scores = []
|
|
362
|
+
for row in trace_rows:
|
|
363
|
+
memory = row.get("memory") if isinstance(row.get("memory"), dict) else {}
|
|
364
|
+
ids = memory.get("recalled_ids")
|
|
365
|
+
ids = [str(item) for item in ids] if isinstance(ids, list) else []
|
|
366
|
+
hits_total += len(ids)
|
|
367
|
+
top = memory.get("top_score")
|
|
368
|
+
if top is None:
|
|
369
|
+
listed = memory.get("scores")
|
|
370
|
+
top = listed[0] if isinstance(listed, list) and listed else None
|
|
371
|
+
if top is not None:
|
|
372
|
+
scores.append(_num(top, 0.0))
|
|
373
|
+
if not ids:
|
|
374
|
+
continue
|
|
375
|
+
current = _session_affect(store, row.get("session_id"))
|
|
376
|
+
if current is None:
|
|
377
|
+
continue
|
|
378
|
+
for entry_id in ids:
|
|
379
|
+
stored = _entry_affect(by_id.get(entry_id))
|
|
380
|
+
if stored is not None and _cosine(stored, current) >= 0.3:
|
|
381
|
+
mood_match_hits += 1
|
|
382
|
+
break
|
|
383
|
+
|
|
384
|
+
rel = relationship(store)
|
|
385
|
+
change, basis, base_ts = _change_7d(store, stamp, rel)
|
|
386
|
+
|
|
387
|
+
intents = Counter()
|
|
388
|
+
for row in trace_rows:
|
|
389
|
+
intent = row.get("intent") if isinstance(row.get("intent"), dict) else {}
|
|
390
|
+
kind = str(intent.get("kind") or "").strip()
|
|
391
|
+
if kind:
|
|
392
|
+
intents[kind] += 1
|
|
393
|
+
narrative = [{"kind": kind, "count": count}
|
|
394
|
+
for kind, count in intents.most_common(SECTION_TOP)]
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
"memory": {
|
|
398
|
+
"total": total,
|
|
399
|
+
"added_this_week": added,
|
|
400
|
+
"avg_importance": avg_importance,
|
|
401
|
+
"with_affect": with_affect,
|
|
402
|
+
"with_affect_ratio": (round(with_affect / total, 4)
|
|
403
|
+
if total else 0.0),
|
|
404
|
+
},
|
|
405
|
+
"recall": {
|
|
406
|
+
"hits_total": hits_total,
|
|
407
|
+
"mood_match_hits": mood_match_hits,
|
|
408
|
+
"avg_top_score": (round(sum(scores) / len(scores), 4)
|
|
409
|
+
if scores else None),
|
|
410
|
+
"score_source": "trace" if scores else "unavailable",
|
|
411
|
+
"traces": len(trace_rows),
|
|
412
|
+
},
|
|
413
|
+
"relation": {
|
|
414
|
+
"trust": round(_num(rel.get("trust"), 0.5), 4),
|
|
415
|
+
"closeness": round(_num(rel.get("closeness"), 0.5), 4),
|
|
416
|
+
"change_7d": change,
|
|
417
|
+
"change_basis": basis,
|
|
418
|
+
"change_base_ts": base_ts,
|
|
419
|
+
"days_known": int(_num(rel.get("days_known"), 0)),
|
|
420
|
+
"attachment_style": str(rel.get("attachment_style") or ""),
|
|
421
|
+
},
|
|
422
|
+
"narrative": {"top_intents": narrative,
|
|
423
|
+
"traces": len(trace_rows)},
|
|
424
|
+
"flags": _flags(store, flags=flags, env=env),
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# ---------------------------------------------------------------- dry-run
|
|
429
|
+
def _topics(text):
|
|
430
|
+
if _drift is not None:
|
|
431
|
+
try:
|
|
432
|
+
return _drift.topics(text)
|
|
433
|
+
except Exception:
|
|
434
|
+
pass
|
|
435
|
+
return []
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def candidates(store, limit=DEFAULT_EPISODES):
|
|
439
|
+
"""Candidate facts from the newest N episodes; never mutates anything."""
|
|
440
|
+
rows = episodes(store, limit)
|
|
441
|
+
counts = Counter()
|
|
442
|
+
evidence = {}
|
|
443
|
+
for entry in rows:
|
|
444
|
+
for topic in dict.fromkeys(_topics(str(entry.get("text") or ""))):
|
|
445
|
+
counts[topic] += 1
|
|
446
|
+
evidence.setdefault(topic, [])
|
|
447
|
+
entry_id = str(entry.get("id") or "")
|
|
448
|
+
if entry_id and entry_id not in evidence[topic]:
|
|
449
|
+
evidence[topic].append(entry_id)
|
|
450
|
+
found = []
|
|
451
|
+
for topic, count in counts.most_common():
|
|
452
|
+
if count < TOPIC_MIN_REPEAT:
|
|
453
|
+
continue
|
|
454
|
+
dimension = None
|
|
455
|
+
if _drift is not None:
|
|
456
|
+
dimension = _drift.dimension_of(topic)
|
|
457
|
+
found.append({
|
|
458
|
+
"kind": "repeated_topic",
|
|
459
|
+
"topic": topic,
|
|
460
|
+
"count": int(count),
|
|
461
|
+
"dimension": dimension,
|
|
462
|
+
"confidence": round(min(0.95, 0.4 + 0.1 * count), 3),
|
|
463
|
+
"evidence": evidence.get(topic, [])[:5],
|
|
464
|
+
})
|
|
465
|
+
# a theme that maps onto a value dimension is the useful candidate, so
|
|
466
|
+
# unmapped bigram noise sorts after it on a tie
|
|
467
|
+
found.sort(key=lambda item: (-item["count"], item["dimension"] is None))
|
|
468
|
+
found = found[:MAX_CANDIDATES]
|
|
469
|
+
|
|
470
|
+
rel = relationship(store)
|
|
471
|
+
trust = _num(rel.get("trust"), 0.5)
|
|
472
|
+
closeness = _num(rel.get("closeness"), 0.5)
|
|
473
|
+
conflict = _num(rel.get("conflict"), 0.0)
|
|
474
|
+
if trust >= RELATION_STABLE_MIN and closeness >= RELATION_STABLE_MIN:
|
|
475
|
+
days = int(_num(rel.get("days_known"), 0))
|
|
476
|
+
style = str(rel.get("attachment_style") or "secure")
|
|
477
|
+
fact = ("关系稳定:信任 %.2f、亲近 %.2f(%s)" % (trust, closeness, style))
|
|
478
|
+
if days > 0:
|
|
479
|
+
fact += ",已相处 %d 天" % days
|
|
480
|
+
found.append({
|
|
481
|
+
"kind": "stable_relation",
|
|
482
|
+
"fact": fact,
|
|
483
|
+
"trust": round(trust, 4),
|
|
484
|
+
"closeness": round(closeness, 4),
|
|
485
|
+
"conflict": round(conflict, 4),
|
|
486
|
+
"days_known": days,
|
|
487
|
+
"attachment_style": style,
|
|
488
|
+
"confidence": round(min(0.95, (trust + closeness) / 2.0), 3),
|
|
489
|
+
})
|
|
490
|
+
return found
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def consolidate(store, limit=DEFAULT_EPISODES, now=None, enabled=None,
|
|
494
|
+
flags=None, env=None, write_report=True):
|
|
495
|
+
"""Dry-run consolidation: candidates + report file, memory untouched."""
|
|
496
|
+
stamp = _num(now, time.time())
|
|
497
|
+
on = (bool(enabled) if enabled is not None
|
|
498
|
+
else _enabled(flags=flags, env=env))
|
|
499
|
+
if not on:
|
|
500
|
+
return {"ok": False, "reason": "disabled", "dry_run": True,
|
|
501
|
+
"report": None, "path": ""}
|
|
502
|
+
scanned = episodes(store, limit)
|
|
503
|
+
found = candidates(store, limit)
|
|
504
|
+
counts = Counter(item.get("kind") for item in found)
|
|
505
|
+
report = {
|
|
506
|
+
"schema": "bc.consolidate_report.v1",
|
|
507
|
+
"generated_ts": round(stamp, 3),
|
|
508
|
+
"dry_run": True,
|
|
509
|
+
"memory_written": False,
|
|
510
|
+
"episodes": {"source": "memory_entries", "scanned": len(scanned),
|
|
511
|
+
"limit": max(1, int(_num(limit, DEFAULT_EPISODES)))},
|
|
512
|
+
"candidates": found,
|
|
513
|
+
"counts": {kind: counts.get(kind, 0)
|
|
514
|
+
for kind in ("repeated_topic", "stable_relation")},
|
|
515
|
+
"flags": _flags(store, flags=flags, env=env),
|
|
516
|
+
}
|
|
517
|
+
path = report_path(store)
|
|
518
|
+
written = False
|
|
519
|
+
if write_report and path is not None:
|
|
520
|
+
try:
|
|
521
|
+
_atomic_write(path, report)
|
|
522
|
+
written = True
|
|
523
|
+
_record_history(store, stamp, relationship(store))
|
|
524
|
+
except Exception:
|
|
525
|
+
written = False
|
|
526
|
+
return {"ok": True, "reason": "dry_run", "dry_run": True,
|
|
527
|
+
"report": report, "path": str(path or ""),
|
|
528
|
+
"written": written}
|