kontinuum-core 0.1.1__tar.gz
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.
- kontinuum_core-0.1.1/LICENSE +10 -0
- kontinuum_core-0.1.1/PKG-INFO +46 -0
- kontinuum_core-0.1.1/README.md +28 -0
- kontinuum_core-0.1.1/pyproject.toml +34 -0
- kontinuum_core-0.1.1/setup.cfg +4 -0
- kontinuum_core-0.1.1/src/kontinuum_core/__init__.py +13 -0
- kontinuum_core-0.1.1/src/kontinuum_core/amygdala.py +164 -0
- kontinuum_core-0.1.1/src/kontinuum_core/anterior_cingulate.py +178 -0
- kontinuum_core-0.1.1/src/kontinuum_core/basal_ganglia.py +311 -0
- kontinuum_core-0.1.1/src/kontinuum_core/cerebellum.py +429 -0
- kontinuum_core-0.1.1/src/kontinuum_core/engine.py +206 -0
- kontinuum_core-0.1.1/src/kontinuum_core/entorhinal_cortex.py +51 -0
- kontinuum_core-0.1.1/src/kontinuum_core/hippocampus.py +493 -0
- kontinuum_core-0.1.1/src/kontinuum_core/hypothalamus.py +275 -0
- kontinuum_core-0.1.1/src/kontinuum_core/insula.py +211 -0
- kontinuum_core-0.1.1/src/kontinuum_core/locus_coeruleus.py +35 -0
- kontinuum_core-0.1.1/src/kontinuum_core/metaplasticity.py +213 -0
- kontinuum_core-0.1.1/src/kontinuum_core/neurorhythms.py +280 -0
- kontinuum_core-0.1.1/src/kontinuum_core/nucleus_accumbens.py +41 -0
- kontinuum_core-0.1.1/src/kontinuum_core/predictive_processing.py +238 -0
- kontinuum_core-0.1.1/src/kontinuum_core/prefrontal_cortex.py +421 -0
- kontinuum_core-0.1.1/src/kontinuum_core/reticular.py +87 -0
- kontinuum_core-0.1.1/src/kontinuum_core/scheduler.py +18 -0
- kontinuum_core-0.1.1/src/kontinuum_core/sleep_consolidation.py +276 -0
- kontinuum_core-0.1.1/src/kontinuum_core/spatial_cortex.py +292 -0
- kontinuum_core-0.1.1/src/kontinuum_core/thalamus.py +778 -0
- kontinuum_core-0.1.1/src/kontinuum_core/types.py +24 -0
- kontinuum_core-0.1.1/src/kontinuum_core.egg-info/PKG-INFO +46 -0
- kontinuum_core-0.1.1/src/kontinuum_core.egg-info/SOURCES.txt +35 -0
- kontinuum_core-0.1.1/src/kontinuum_core.egg-info/dependency_links.txt +1 -0
- kontinuum_core-0.1.1/src/kontinuum_core.egg-info/requires.txt +3 -0
- kontinuum_core-0.1.1/src/kontinuum_core.egg-info/top_level.txt +1 -0
- kontinuum_core-0.1.1/tests/test_cerebellum.py +77 -0
- kontinuum_core-0.1.1/tests/test_engine.py +190 -0
- kontinuum_core-0.1.1/tests/test_hippocampus.py +112 -0
- kontinuum_core-0.1.1/tests/test_predictive_processing.py +148 -0
- kontinuum_core-0.1.1/tests/test_thalamus.py +75 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 3, 19 November 2007
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
5
|
+
Everyone is permitted to copy and distribute verbatim copies
|
|
6
|
+
of this license document, but changing it is not allowed.
|
|
7
|
+
|
|
8
|
+
This repository uses the GNU Affero General Public License v3.0.
|
|
9
|
+
For the full license text, see:
|
|
10
|
+
https://www.gnu.org/licenses/agpl-3.0.txt
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kontinuum-core
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Neuro-inspired learning engine for anomaly detection
|
|
5
|
+
Author-email: Chance-Konstruktion <introareback@gmail.com>
|
|
6
|
+
License: AGPL-3.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/Chance-Konstruktion/kontinuum-core
|
|
8
|
+
Project-URL: Issues, https://github.com/Chance-Konstruktion/kontinuum-core/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# KONTINUUM Core
|
|
20
|
+
|
|
21
|
+
Pure Python learning engine extracted from [KONTINUUM](https://github.com/Chance-Konstruktion/ha-kontinuum). No Home Assistant dependency — usable from any Python project.
|
|
22
|
+
|
|
23
|
+
> **Part of the 3-repo family:**
|
|
24
|
+
> **kontinuum-core** (this repo, HA-free Python package on PyPI) ·
|
|
25
|
+
> [`ha-kontinuum`](https://github.com/Chance-Konstruktion/ha-kontinuum) (full HA Pro integration with UI) ·
|
|
26
|
+
> [`ha-kontinuum-lite`](https://github.com/Chance-Konstruktion/ha-kontinuum-lite) (slim HA integration, no UI)
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install kontinuum-core
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from kontinuum_core import KontinuumEngine
|
|
38
|
+
|
|
39
|
+
engine = KontinuumEngine()
|
|
40
|
+
snapshot = engine.observe({"token": "bedroom.light.on", "room": "bedroom"})
|
|
41
|
+
print(snapshot.surprise)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
AGPL-3.0 – see LICENSE file.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# KONTINUUM Core
|
|
2
|
+
|
|
3
|
+
Pure Python learning engine extracted from [KONTINUUM](https://github.com/Chance-Konstruktion/ha-kontinuum). No Home Assistant dependency — usable from any Python project.
|
|
4
|
+
|
|
5
|
+
> **Part of the 3-repo family:**
|
|
6
|
+
> **kontinuum-core** (this repo, HA-free Python package on PyPI) ·
|
|
7
|
+
> [`ha-kontinuum`](https://github.com/Chance-Konstruktion/ha-kontinuum) (full HA Pro integration with UI) ·
|
|
8
|
+
> [`ha-kontinuum-lite`](https://github.com/Chance-Konstruktion/ha-kontinuum-lite) (slim HA integration, no UI)
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install kontinuum-core
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from kontinuum_core import KontinuumEngine
|
|
20
|
+
|
|
21
|
+
engine = KontinuumEngine()
|
|
22
|
+
snapshot = engine.observe({"token": "bedroom.light.on", "room": "bedroom"})
|
|
23
|
+
print(snapshot.surprise)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## License
|
|
27
|
+
|
|
28
|
+
AGPL-3.0 – see LICENSE file.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kontinuum-core"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
authors = [{name = "Chance-Konstruktion", email = "introareback@gmail.com"}]
|
|
9
|
+
description = "Neuro-inspired learning engine for anomaly detection"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = {text = "AGPL-3.0"}
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: GNU Affero General Public License v3",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://github.com/Chance-Konstruktion/kontinuum-core"
|
|
21
|
+
Issues = "https://github.com/Chance-Konstruktion/kontinuum-core/issues"
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = ["pytest>=7"]
|
|
25
|
+
|
|
26
|
+
[tool.setuptools]
|
|
27
|
+
package-dir = {"" = "src"}
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
testpaths = ["tests"]
|
|
34
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""KONTINUUM Core - neuro-inspired learning engine (HA-free)."""
|
|
2
|
+
from .engine import KontinuumEngine
|
|
3
|
+
from .scheduler import Scheduler
|
|
4
|
+
from .types import MemoryState, Observation, Prediction
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.1"
|
|
7
|
+
__all__ = [
|
|
8
|
+
"KontinuumEngine",
|
|
9
|
+
"Scheduler",
|
|
10
|
+
"Observation",
|
|
11
|
+
"Prediction",
|
|
12
|
+
"MemoryState",
|
|
13
|
+
]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Amygdala module for KONTINUUM Core.
|
|
2
|
+
|
|
3
|
+
Biological inspiration: Risk assessment and safety gate. The amygdala
|
|
4
|
+
evaluates proposed actions emotionally – above all for danger – and can
|
|
5
|
+
trigger a rapid "STOP!" before the prefrontal cortex has finished
|
|
6
|
+
reasoning.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
_LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
NEVER_AUTO = {"alarm", "lock"}
|
|
15
|
+
|
|
16
|
+
RISK_SCORES = {
|
|
17
|
+
"light": 0.05, "media": 0.05, "automation": 0.1,
|
|
18
|
+
"switch": 0.2, "fan": 0.15, "cover": 0.3,
|
|
19
|
+
"climate": 0.35, "water_heater": 0.4, "vacuum": 0.25,
|
|
20
|
+
"lock": 0.9, "alarm": 1.0,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
SAFE_STATES = {
|
|
24
|
+
"light": {"on", "off"},
|
|
25
|
+
"media": {"playing", "paused", "standby", "off"},
|
|
26
|
+
"switch": {"on", "off"},
|
|
27
|
+
"fan": {"on", "off"},
|
|
28
|
+
"climate": {"heating", "cooling", "idle", "off"},
|
|
29
|
+
"cover": {"open", "closed"},
|
|
30
|
+
"automation": {"on"},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Amygdala:
|
|
35
|
+
"""Assesses the risk of a proposed action."""
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
self.learned_risk = {}
|
|
39
|
+
self.veto_log = []
|
|
40
|
+
self.total_assessments = 0
|
|
41
|
+
self.total_vetoes = 0
|
|
42
|
+
self.total_cautions = 0
|
|
43
|
+
|
|
44
|
+
def assess(self, token: str, semantic: str, room: str,
|
|
45
|
+
state: str, confidence: float,
|
|
46
|
+
context: dict = None) -> dict:
|
|
47
|
+
"""Assesses the risk of an action."""
|
|
48
|
+
self.total_assessments += 1
|
|
49
|
+
context = context or {}
|
|
50
|
+
reasons = []
|
|
51
|
+
|
|
52
|
+
# 1. Absolute vetoes
|
|
53
|
+
if semantic in NEVER_AUTO:
|
|
54
|
+
self.total_vetoes += 1
|
|
55
|
+
reason = f"{semantic} is safety-critical"
|
|
56
|
+
reasons.append(reason)
|
|
57
|
+
self._log_veto(token, reason)
|
|
58
|
+
return {
|
|
59
|
+
"decision": "VETO", "risk": 1.0,
|
|
60
|
+
"reasons": reasons, "required_confidence": float("inf"),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
system_mode = context.get("system_mode", "home")
|
|
64
|
+
if system_mode in ("away", "vacation"):
|
|
65
|
+
if semantic not in ("automation",):
|
|
66
|
+
reasons.append(f"System in {system_mode} mode")
|
|
67
|
+
|
|
68
|
+
# 2. Base risk
|
|
69
|
+
base_risk = RISK_SCORES.get(semantic, 0.3)
|
|
70
|
+
|
|
71
|
+
# 2b. SAFE_STATES check
|
|
72
|
+
if semantic in SAFE_STATES:
|
|
73
|
+
if state not in SAFE_STATES[semantic]:
|
|
74
|
+
base_risk = min(1.0, base_risk + 0.2)
|
|
75
|
+
reasons.append(f"Unknown state '{state}' for {semantic}: +0.2")
|
|
76
|
+
|
|
77
|
+
# 3. Context modifiers
|
|
78
|
+
risk = base_risk
|
|
79
|
+
|
|
80
|
+
if system_mode in ("away", "vacation"):
|
|
81
|
+
risk = min(1.0, risk + 0.3)
|
|
82
|
+
reasons.append("Away mode: +0.3 risk")
|
|
83
|
+
|
|
84
|
+
is_night = context.get("is_night", False)
|
|
85
|
+
if is_night and semantic == "light" and state == "on":
|
|
86
|
+
risk = min(1.0, risk + 0.1)
|
|
87
|
+
reasons.append("Light on at night: +0.1")
|
|
88
|
+
|
|
89
|
+
learned_key = f"{room}.{semantic}.{state}"
|
|
90
|
+
if learned_key in self.learned_risk:
|
|
91
|
+
modifier = self.learned_risk[learned_key]
|
|
92
|
+
risk = max(0.0, min(1.0, risk + modifier))
|
|
93
|
+
reasons.append(f"Learned: {modifier:+.2f}")
|
|
94
|
+
|
|
95
|
+
# 4. Confidence thresholds
|
|
96
|
+
if risk < 0.15:
|
|
97
|
+
required_conf = 0.3
|
|
98
|
+
elif risk < 0.3:
|
|
99
|
+
required_conf = 0.5
|
|
100
|
+
elif risk < 0.5:
|
|
101
|
+
required_conf = 0.65
|
|
102
|
+
elif risk < 0.7:
|
|
103
|
+
required_conf = 0.8
|
|
104
|
+
else:
|
|
105
|
+
required_conf = 0.95
|
|
106
|
+
|
|
107
|
+
# 5. Decision
|
|
108
|
+
if risk >= 0.95:
|
|
109
|
+
decision = "VETO"
|
|
110
|
+
self.total_vetoes += 1
|
|
111
|
+
reasons.append(f"Risk {risk:.2f} >= 0.95")
|
|
112
|
+
self._log_veto(token, reasons[-1])
|
|
113
|
+
elif confidence < required_conf:
|
|
114
|
+
decision = "CAUTION"
|
|
115
|
+
self.total_cautions += 1
|
|
116
|
+
reasons.append(f"Conf {confidence:.0%} < {required_conf:.0%}")
|
|
117
|
+
else:
|
|
118
|
+
decision = "ALLOW"
|
|
119
|
+
reasons.append(f"Conf {confidence:.0%} >= {required_conf:.0%}")
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
"decision": decision, "risk": round(risk, 3),
|
|
123
|
+
"reasons": reasons, "required_confidence": required_conf,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
def learn_from_feedback(self, token: str, feedback: str):
|
|
127
|
+
"""Adjusts risk based on user feedback."""
|
|
128
|
+
if feedback == "negative":
|
|
129
|
+
current = self.learned_risk.get(token, 0.0)
|
|
130
|
+
self.learned_risk[token] = min(0.5, current + 0.05)
|
|
131
|
+
elif feedback == "positive":
|
|
132
|
+
current = self.learned_risk.get(token, 0.0)
|
|
133
|
+
self.learned_risk[token] = max(-0.2, current - 0.02)
|
|
134
|
+
|
|
135
|
+
def _log_veto(self, token: str, reason: str):
|
|
136
|
+
self.veto_log.append({"timestamp": time.time(), "token": token, "reason": reason})
|
|
137
|
+
if len(self.veto_log) > 50:
|
|
138
|
+
self.veto_log = self.veto_log[-50:]
|
|
139
|
+
_LOGGER.info("VETO: %s – %s", token, reason)
|
|
140
|
+
|
|
141
|
+
def to_dict(self) -> dict:
|
|
142
|
+
return {
|
|
143
|
+
"learned_risk": self.learned_risk,
|
|
144
|
+
"total_assessments": self.total_assessments,
|
|
145
|
+
"total_vetoes": self.total_vetoes,
|
|
146
|
+
"total_cautions": self.total_cautions,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
def from_dict(self, data: dict):
|
|
150
|
+
self.learned_risk = data.get("learned_risk", {})
|
|
151
|
+
self.total_assessments = data.get("total_assessments", 0)
|
|
152
|
+
self.total_vetoes = data.get("total_vetoes", 0)
|
|
153
|
+
self.total_cautions = data.get("total_cautions", 0)
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def stats(self) -> dict:
|
|
157
|
+
return {
|
|
158
|
+
"total_assessments": self.total_assessments,
|
|
159
|
+
"total_vetoes": self.total_vetoes,
|
|
160
|
+
"total_cautions": self.total_cautions,
|
|
161
|
+
"learned_risks": len(self.learned_risk),
|
|
162
|
+
"veto_rate": f"{self.total_vetoes / max(1, self.total_assessments):.1%}",
|
|
163
|
+
"recent_vetos": self.veto_log[-5:],
|
|
164
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Anterior Cingulate Cortex (ACC) – conflict monitor and error tracker.
|
|
2
|
+
|
|
3
|
+
Biological inspiration: The ACC detects conflicts between competing
|
|
4
|
+
action candidates and signals when uncertainty is too high. It
|
|
5
|
+
modulates the decision threshold: many conflicts → act more cautiously,
|
|
6
|
+
high clarity → act faster.
|
|
7
|
+
|
|
8
|
+
Responsibilities:
|
|
9
|
+
- Detect conflicts between modules (e.g. Hippocampus vs Amygdala)
|
|
10
|
+
- Track prediction errors (expectation vs reality)
|
|
11
|
+
- Adjust confidence thresholds dynamically
|
|
12
|
+
- Signal "cognitive control" when uncertainty is high
|
|
13
|
+
|
|
14
|
+
Performance: pure arithmetic, no I/O, no ML. ~0 ms per event.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from collections import deque
|
|
19
|
+
|
|
20
|
+
_LOGGER = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# How many decisions to keep in the rolling window
|
|
23
|
+
HISTORY_SIZE = 200
|
|
24
|
+
# EMA factor for conflict rate
|
|
25
|
+
EMA_ALPHA = 0.05
|
|
26
|
+
# Thresholds for conflict level
|
|
27
|
+
CONFLICT_LOW = 0.2
|
|
28
|
+
CONFLICT_HIGH = 0.6
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AnteriorCingulateCortex:
|
|
32
|
+
"""Monitors conflicts between modules and adapts decision thresholds."""
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self._conflict_history = deque(maxlen=HISTORY_SIZE)
|
|
36
|
+
self._error_history = deque(maxlen=HISTORY_SIZE)
|
|
37
|
+
|
|
38
|
+
self.conflict_level = 0.0 # 0.0 = no conflict, 1.0 = max conflict
|
|
39
|
+
self.error_rate = 0.0 # fraction of wrong predictions
|
|
40
|
+
self.cognitive_control = 0.0 # how hard to brake
|
|
41
|
+
|
|
42
|
+
self.total_conflicts = 0
|
|
43
|
+
self.total_agreements = 0
|
|
44
|
+
self.total_errors = 0
|
|
45
|
+
self.total_correct = 0
|
|
46
|
+
self.threshold_adjustments = 0
|
|
47
|
+
|
|
48
|
+
self.confidence_threshold = 0.5
|
|
49
|
+
self._last_update_ts = 0.0
|
|
50
|
+
|
|
51
|
+
def observe_decision(self, proposals: list) -> float:
|
|
52
|
+
"""Measures the conflict level of a decision round.
|
|
53
|
+
|
|
54
|
+
proposals: list of module suggestions, e.g.:
|
|
55
|
+
[{"source": "hippocampus", "action": "light.on", "confidence": 0.8},
|
|
56
|
+
{"source": "amygdala", "action": "veto", "confidence": 0.6}]
|
|
57
|
+
|
|
58
|
+
Returns: conflict value in [0.0, 1.0]
|
|
59
|
+
"""
|
|
60
|
+
if not proposals or len(proposals) < 2:
|
|
61
|
+
self._conflict_history.append(0.0)
|
|
62
|
+
self._update_ema()
|
|
63
|
+
self.total_agreements += 1
|
|
64
|
+
return 0.0
|
|
65
|
+
|
|
66
|
+
actions = [p.get("action", "") for p in proposals]
|
|
67
|
+
unique_actions = set(actions)
|
|
68
|
+
|
|
69
|
+
disagreement = (len(unique_actions) - 1) / max(1, len(actions) - 1)
|
|
70
|
+
|
|
71
|
+
has_veto = any(p.get("action") == "veto" or p.get("veto", False) for p in proposals)
|
|
72
|
+
if has_veto:
|
|
73
|
+
disagreement = min(1.0, disagreement + 0.3)
|
|
74
|
+
|
|
75
|
+
confidences = [p.get("confidence", 0.5) for p in proposals]
|
|
76
|
+
if len(confidences) > 1:
|
|
77
|
+
mean_conf = sum(confidences) / len(confidences)
|
|
78
|
+
variance = sum((c - mean_conf) ** 2 for c in confidences) / len(confidences)
|
|
79
|
+
disagreement = min(1.0, disagreement + variance * 0.5)
|
|
80
|
+
|
|
81
|
+
self._conflict_history.append(disagreement)
|
|
82
|
+
self._update_ema()
|
|
83
|
+
|
|
84
|
+
if disagreement > CONFLICT_LOW:
|
|
85
|
+
self.total_conflicts += 1
|
|
86
|
+
else:
|
|
87
|
+
self.total_agreements += 1
|
|
88
|
+
|
|
89
|
+
return disagreement
|
|
90
|
+
|
|
91
|
+
def observe_outcome(self, was_correct: bool):
|
|
92
|
+
"""Records whether the last action was correct."""
|
|
93
|
+
self._error_history.append(0.0 if was_correct else 1.0)
|
|
94
|
+
|
|
95
|
+
if was_correct:
|
|
96
|
+
self.total_correct += 1
|
|
97
|
+
else:
|
|
98
|
+
self.total_errors += 1
|
|
99
|
+
|
|
100
|
+
self.error_rate = (
|
|
101
|
+
EMA_ALPHA * (0.0 if was_correct else 1.0)
|
|
102
|
+
+ (1.0 - EMA_ALPHA) * self.error_rate
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
self._adjust_threshold()
|
|
106
|
+
|
|
107
|
+
def get_adjusted_threshold(self, base_confidence: float = 0.5) -> float:
|
|
108
|
+
"""Returns an adjusted confidence threshold.
|
|
109
|
+
|
|
110
|
+
High conflict → threshold raised (more cautious).
|
|
111
|
+
Low conflict → threshold lowered (act faster).
|
|
112
|
+
"""
|
|
113
|
+
return max(0.2, min(0.95, base_confidence + self.cognitive_control * 0.3))
|
|
114
|
+
|
|
115
|
+
def should_defer_to_cortex(self) -> bool:
|
|
116
|
+
"""Returns True when uncertainty is high enough to consult the cortex (LLM)."""
|
|
117
|
+
return self.conflict_level > CONFLICT_HIGH or self.error_rate > 0.4
|
|
118
|
+
|
|
119
|
+
def _update_ema(self):
|
|
120
|
+
if self._conflict_history:
|
|
121
|
+
latest = self._conflict_history[-1]
|
|
122
|
+
self.conflict_level = (
|
|
123
|
+
EMA_ALPHA * latest + (1.0 - EMA_ALPHA) * self.conflict_level
|
|
124
|
+
)
|
|
125
|
+
self.cognitive_control = min(1.0, self.conflict_level * 0.6 + self.error_rate * 0.4)
|
|
126
|
+
|
|
127
|
+
def _adjust_threshold(self):
|
|
128
|
+
old = self.confidence_threshold
|
|
129
|
+
|
|
130
|
+
if self.error_rate > 0.3:
|
|
131
|
+
self.confidence_threshold = min(0.9, self.confidence_threshold + 0.02)
|
|
132
|
+
elif self.error_rate < 0.1 and self.conflict_level < CONFLICT_LOW:
|
|
133
|
+
self.confidence_threshold = max(0.3, self.confidence_threshold - 0.01)
|
|
134
|
+
|
|
135
|
+
if abs(old - self.confidence_threshold) > 0.001:
|
|
136
|
+
self.threshold_adjustments += 1
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def stats(self) -> dict:
|
|
140
|
+
return {
|
|
141
|
+
"conflict_level": round(self.conflict_level, 3),
|
|
142
|
+
"error_rate": round(self.error_rate, 3),
|
|
143
|
+
"cognitive_control": round(self.cognitive_control, 3),
|
|
144
|
+
"confidence_threshold": round(self.confidence_threshold, 3),
|
|
145
|
+
"total_conflicts": self.total_conflicts,
|
|
146
|
+
"total_agreements": self.total_agreements,
|
|
147
|
+
"total_errors": self.total_errors,
|
|
148
|
+
"total_correct": self.total_correct,
|
|
149
|
+
"threshold_adjustments": self.threshold_adjustments,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
def to_dict(self) -> dict:
|
|
153
|
+
return {
|
|
154
|
+
"conflict_level": self.conflict_level,
|
|
155
|
+
"error_rate": self.error_rate,
|
|
156
|
+
"cognitive_control": self.cognitive_control,
|
|
157
|
+
"confidence_threshold": self.confidence_threshold,
|
|
158
|
+
"total_conflicts": self.total_conflicts,
|
|
159
|
+
"total_agreements": self.total_agreements,
|
|
160
|
+
"total_errors": self.total_errors,
|
|
161
|
+
"total_correct": self.total_correct,
|
|
162
|
+
"threshold_adjustments": self.threshold_adjustments,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
def from_dict(self, data: dict):
|
|
166
|
+
self.conflict_level = data.get("conflict_level", 0.0)
|
|
167
|
+
self.error_rate = data.get("error_rate", 0.0)
|
|
168
|
+
self.cognitive_control = data.get("cognitive_control", 0.0)
|
|
169
|
+
self.confidence_threshold = data.get("confidence_threshold", 0.5)
|
|
170
|
+
self.total_conflicts = data.get("total_conflicts", 0)
|
|
171
|
+
self.total_agreements = data.get("total_agreements", 0)
|
|
172
|
+
self.total_errors = data.get("total_errors", 0)
|
|
173
|
+
self.total_correct = data.get("total_correct", 0)
|
|
174
|
+
self.threshold_adjustments = data.get("threshold_adjustments", 0)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# Backwards-compatible alias for engine.py imports.
|
|
178
|
+
AnteriorCingulate = AnteriorCingulateCortex
|