~JADIS
commited on
Commit
·
a3bcd92
1
Parent(s):
bc9efb0
Establish conscious heart core with local-first policy
Browse files- README.md +8 -0
- blux/adaptors/__init__.py +7 -1
- blux/agent/audit.py +21 -13
- blux/agent/constitution.py +13 -9
- blux/agent/core_agent.py +15 -19
- blux/agent/discernment.py +15 -10
- blux/agent/memory.py +20 -14
- blux/orchestrator/controller.py +39 -41
- blux/orchestrator/logs.py +1 -0
- blux/orchestrator/registry.py +15 -9
- blux_ca/adapters/__init__.py +5 -1
- blux_ca/adapters/bq_cli.py +55 -0
- blux_ca/core/__init__.py +14 -0
- blux_ca/core/audit.py +96 -6
- blux_ca/core/compass/__init__.py +10 -0
- blux_ca/core/compass/intent.py +83 -0
- blux_ca/core/discernment.py +12 -5
- blux_ca/core/heart.py +126 -0
- blux_ca/core/koan.py +54 -0
- blux_ca/core/memory.py +85 -0
- tests/ca/test_audit.py +19 -1
- tests/ca/test_bq_cli.py +9 -0
- tests/ca/test_compass.py +14 -0
- tests/ca/test_heart.py +17 -0
- tests/ca/test_memory.py +19 -0
README.md
CHANGED
|
@@ -16,6 +16,14 @@ Memory decay for outdated information
|
|
| 16 |
|
| 17 |
Recall, filtering, and summarization
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
Multi-Agent Collaboration
|
| 20 |
|
| 21 |
Broadcast tasks to multiple agents
|
|
|
|
| 16 |
|
| 17 |
Recall, filtering, and summarization
|
| 18 |
|
| 19 |
+
**Phase 1 Conscious Heart**
|
| 20 |
+
|
| 21 |
+
- `blux_ca.core.heart.ConsciousHeart` orchestrates perception, discernment, and doctrine-backed verdicts.
|
| 22 |
+
- Intent compass classifies along truth, integrity, compassion, and awareness axes.
|
| 23 |
+
- Koan probes surface reflective prompts for orchestrated `bq-cli` sessions.
|
| 24 |
+
- Consent-based memory honors privacy-first defaults and only persists with explicit approval.
|
| 25 |
+
- Immutable audit log supports append-only playback with hash chaining for local-first compliance.
|
| 26 |
+
|
| 27 |
Multi-Agent Collaboration
|
| 28 |
|
| 29 |
Broadcast tasks to multiple agents
|
blux/adaptors/__init__.py
CHANGED
|
@@ -1,3 +1,9 @@
|
|
| 1 |
# Adaptors package
|
| 2 |
from .dummy_local import DummyLocalAdaptor
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Adaptors package
|
| 2 |
from .dummy_local import DummyLocalAdaptor
|
| 3 |
+
|
| 4 |
+
try: # Optional HTTP adaptor dependency
|
| 5 |
+
from .http_api_adaptor import HTTPAPIAdaptor
|
| 6 |
+
except ModuleNotFoundError: # pragma: no cover - optional flask integration
|
| 7 |
+
HTTPAPIAdaptor = None # type: ignore
|
| 8 |
+
|
| 9 |
+
__all__ = ["DummyLocalAdaptor", "HTTPAPIAdaptor"]
|
blux/agent/audit.py
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
class AuditTrail:
|
| 2 |
-
"""
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
self.logs = []
|
| 7 |
|
| 8 |
-
def log(self, user_input, user_type, decision):
|
| 9 |
-
self.logs.append(
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
| 14 |
|
| 15 |
-
def get_logs(self):
|
| 16 |
-
return self.logs
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
"""Simple in-memory audit trail."""
|
| 4 |
+
|
| 5 |
+
from typing import Dict, List
|
| 6 |
+
|
| 7 |
+
|
| 8 |
class AuditTrail:
|
| 9 |
+
"""Records agent decisions and actions in memory."""
|
| 10 |
+
|
| 11 |
+
def __init__(self) -> None:
|
| 12 |
+
self.logs: List[Dict[str, str]] = []
|
|
|
|
| 13 |
|
| 14 |
+
def log(self, user_input: str, user_type: str, decision: str) -> None:
|
| 15 |
+
self.logs.append(
|
| 16 |
+
{
|
| 17 |
+
"input": user_input,
|
| 18 |
+
"user_type": user_type,
|
| 19 |
+
"decision": decision,
|
| 20 |
+
}
|
| 21 |
+
)
|
| 22 |
|
| 23 |
+
def get_logs(self) -> List[Dict[str, str]]:
|
| 24 |
+
return list(self.logs)
|
blux/agent/constitution.py
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
class Constitution:
|
| 2 |
-
"""
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
def __init__(self):
|
| 6 |
self.rules = [
|
| 7 |
"truth_over_comfort",
|
| 8 |
"integrity_over_approval",
|
| 9 |
-
"audit_required_for_decisions"
|
| 10 |
]
|
| 11 |
|
| 12 |
-
def apply_rules(self, user_input, user_type):
|
|
|
|
|
|
|
| 13 |
if user_type == "struggler":
|
| 14 |
return "validate and provide guidance"
|
| 15 |
-
return "set boundaries / off-ramp"
|
| 16 |
-
else:
|
| 17 |
-
return "set boundaries / off-ramp"
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
"""Minimal constitution facade for the basic BLUX agent."""
|
| 4 |
+
|
| 5 |
+
|
| 6 |
class Constitution:
|
| 7 |
+
"""Applies constitution rules based on the classified user type."""
|
| 8 |
+
|
| 9 |
+
def __init__(self) -> None:
|
|
|
|
| 10 |
self.rules = [
|
| 11 |
"truth_over_comfort",
|
| 12 |
"integrity_over_approval",
|
| 13 |
+
"audit_required_for_decisions",
|
| 14 |
]
|
| 15 |
|
| 16 |
+
def apply_rules(self, user_input: str, user_type: str) -> str:
|
| 17 |
+
"""Return the decision aligned with the active constitution."""
|
| 18 |
+
|
| 19 |
if user_type == "struggler":
|
| 20 |
return "validate and provide guidance"
|
| 21 |
+
return "set boundaries / off-ramp"
|
|
|
|
|
|
blux/agent/core_agent.py
CHANGED
|
@@ -1,32 +1,28 @@
|
|
| 1 |
-
from
|
| 2 |
-
|
| 3 |
-
|
|
|
|
| 4 |
from .audit import AuditTrail
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
class BLUXAgent:
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
-
|
| 10 |
-
- Discernment compass
|
| 11 |
-
- Memory handling
|
| 12 |
-
- Auditing
|
| 13 |
-
"""
|
| 14 |
-
def __init__(self, name="BLUX-cA"):
|
| 15 |
self.name = name
|
| 16 |
self.memory = Memory()
|
| 17 |
self.constitution = Constitution()
|
| 18 |
self.discernment = DiscernmentCompass()
|
| 19 |
self.audit = AuditTrail()
|
| 20 |
|
| 21 |
-
def process_input(self, user_input):
|
|
|
|
|
|
|
| 22 |
user_type = self.discernment.classify(user_input)
|
| 23 |
decision = self.constitution.apply_rules(user_input, user_type)
|
| 24 |
self.memory.store(user_input, user_type, decision)
|
| 25 |
self.audit.log(user_input, user_type, decision)
|
| 26 |
-
return f"[{user_type}] Decision: {decision}"
|
| 27 |
-
|
| 28 |
-
# 4. Record audit
|
| 29 |
-
self.audit.log(user_input, user_type, decision)
|
| 30 |
-
|
| 31 |
-
# 5. Generate output (placeholder)
|
| 32 |
-
return f"[{user_type}] Decision: {decision}"
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
"""Legacy BLUX agent wiring used by unit tests."""
|
| 4 |
+
|
| 5 |
from .audit import AuditTrail
|
| 6 |
+
from .constitution import Constitution
|
| 7 |
+
from .discernment import DiscernmentCompass
|
| 8 |
+
from .memory import Memory
|
| 9 |
+
|
| 10 |
|
| 11 |
class BLUXAgent:
|
| 12 |
+
"""Minimal agent coordinating constitution, discernment, memory, and audit."""
|
| 13 |
+
|
| 14 |
+
def __init__(self, name: str = "BLUX-cA") -> None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
self.name = name
|
| 16 |
self.memory = Memory()
|
| 17 |
self.constitution = Constitution()
|
| 18 |
self.discernment = DiscernmentCompass()
|
| 19 |
self.audit = AuditTrail()
|
| 20 |
|
| 21 |
+
def process_input(self, user_input: str) -> str:
|
| 22 |
+
"""Run the full decision flow for ``user_input``."""
|
| 23 |
+
|
| 24 |
user_type = self.discernment.classify(user_input)
|
| 25 |
decision = self.constitution.apply_rules(user_input, user_type)
|
| 26 |
self.memory.store(user_input, user_type, decision)
|
| 27 |
self.audit.log(user_input, user_type, decision)
|
| 28 |
+
return f"[{user_type}] Decision: {decision}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
blux/agent/discernment.py
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
class DiscernmentCompass:
|
| 2 |
-
"""
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
| 9 |
return "struggler"
|
| 10 |
-
return "indulgent"
|
| 11 |
-
else:
|
| 12 |
-
return "indulgent"
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
"""Simple discernment compass for legacy BLUX agent tests."""
|
| 4 |
+
|
| 5 |
+
|
| 6 |
class DiscernmentCompass:
|
| 7 |
+
"""Classifies user intent into coarse support buckets."""
|
| 8 |
+
|
| 9 |
+
_SUPPORT_KEYWORDS = {"help", "struggle", "problem", "support", "lost"}
|
| 10 |
+
|
| 11 |
+
def classify(self, user_input: str) -> str:
|
| 12 |
+
"""Return ``"struggler"`` if supportive keywords are present."""
|
| 13 |
+
|
| 14 |
+
lowered = user_input.lower()
|
| 15 |
+
if any(keyword in lowered for keyword in self._SUPPORT_KEYWORDS):
|
| 16 |
return "struggler"
|
| 17 |
+
return "indulgent"
|
|
|
|
|
|
blux/agent/memory.py
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
class Memory:
|
| 2 |
-
"""
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
self.
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
def store(self, user_input, user_type, decision):
|
| 10 |
entry = {
|
| 11 |
"input": user_input,
|
| 12 |
"user_type": user_type,
|
| 13 |
-
"decision": decision
|
| 14 |
}
|
| 15 |
self.session_memory.append(entry)
|
| 16 |
-
self.long_term_memory.append(entry)
|
| 17 |
|
| 18 |
-
def recall_session(self):
|
| 19 |
-
return self.session_memory
|
| 20 |
|
| 21 |
-
def recall_long_term(self):
|
| 22 |
-
return self.
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
"""In-memory storage used by legacy BLUX agent tests."""
|
| 4 |
+
|
| 5 |
+
from typing import Dict, List
|
| 6 |
+
|
| 7 |
+
|
| 8 |
class Memory:
|
| 9 |
+
"""Stores session and long-term memory entries."""
|
| 10 |
+
|
| 11 |
+
def __init__(self) -> None:
|
| 12 |
+
self.session_memory: List[Dict[str, str]] = []
|
| 13 |
+
self.long_term_memory: List[Dict[str, str]] = []
|
| 14 |
+
|
| 15 |
+
def store(self, user_input: str, user_type: str, decision: str) -> None:
|
|
|
|
| 16 |
entry = {
|
| 17 |
"input": user_input,
|
| 18 |
"user_type": user_type,
|
| 19 |
+
"decision": decision,
|
| 20 |
}
|
| 21 |
self.session_memory.append(entry)
|
| 22 |
+
self.long_term_memory.append(entry)
|
| 23 |
|
| 24 |
+
def recall_session(self) -> List[Dict[str, str]]:
|
| 25 |
+
return list(self.session_memory)
|
| 26 |
|
| 27 |
+
def recall_long_term(self) -> List[Dict[str, str]]:
|
| 28 |
+
return list(self.long_term_memory)
|
blux/orchestrator/controller.py
CHANGED
|
@@ -1,49 +1,47 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
"""
|
| 5 |
-
from typing import List, Dict, Any
|
| 6 |
-
from .registry import ModelRegistry
|
| 7 |
-
from .router import Router
|
| 8 |
-
from .logs import AuditLogger
|
| 9 |
-
from importlib import import_module
|
| 10 |
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
out = adapter.predict(prompt)
|
| 25 |
-
responses.append({"model": name, "output": out})
|
| 26 |
-
|
| 27 |
-
# Evaluate responses
|
| 28 |
-
from .evaluator.python import PythonEvaluator
|
| 29 |
-
|
| 30 |
-
evaluator = PythonEvaluator()
|
| 31 |
-
scored = []
|
| 32 |
-
for r in responses:
|
| 33 |
-
score, metadata = evaluator.score(r["output"], prompt)
|
| 34 |
-
scored.append({"model": r["model"], "output": r["output"], "score": score, "meta": metadata})
|
| 35 |
-
|
| 36 |
-
# Merge policy: choose top score
|
| 37 |
-
best = sorted(scored, key=lambda x: x["score"], reverse=True)[0] if scored else None
|
| 38 |
-
|
| 39 |
-
audit_entry = {
|
| 40 |
-
"prompt": prompt,
|
| 41 |
-
"candidates": scored,
|
| 42 |
-
"selected": best,
|
| 43 |
}
|
| 44 |
-
self.logger.log(audit_entry)
|
| 45 |
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
|
| 49 |
__all__ = ["Controller"]
|
|
|
|
| 1 |
+
"""Minimal orchestration controller used in legacy tests."""
|
| 2 |
|
| 3 |
+
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
+
from dataclasses import dataclass, field
|
| 6 |
+
from typing import Any, Dict
|
| 7 |
|
| 8 |
+
|
| 9 |
+
@dataclass
|
| 10 |
+
class _RegistryState:
|
| 11 |
+
agents: Dict[str, Any] = field(default_factory=dict)
|
| 12 |
+
adaptors: Dict[str, Any] = field(default_factory=dict)
|
| 13 |
+
evaluators: Dict[str, Any] = field(default_factory=dict)
|
| 14 |
+
|
| 15 |
+
def list_all(self) -> Dict[str, list[str]]:
|
| 16 |
+
return {
|
| 17 |
+
"agents": list(self.agents.keys()),
|
| 18 |
+
"adaptors": list(self.adaptors.keys()),
|
| 19 |
+
"evaluators": list(self.evaluators.keys()),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
}
|
|
|
|
| 21 |
|
| 22 |
+
|
| 23 |
+
class Controller:
|
| 24 |
+
"""Coordinates agents, adaptors, and evaluators for simple task processing."""
|
| 25 |
+
|
| 26 |
+
def __init__(self) -> None:
|
| 27 |
+
self.registry = _RegistryState()
|
| 28 |
+
|
| 29 |
+
def register_agent(self, name: str, agent: Any) -> None:
|
| 30 |
+
self.registry.agents[name] = agent
|
| 31 |
+
|
| 32 |
+
def register_adaptor(self, name: str, adaptor: Any) -> None:
|
| 33 |
+
self.registry.adaptors[name] = adaptor
|
| 34 |
+
|
| 35 |
+
def register_evaluator(self, name: str, evaluator: Any) -> None:
|
| 36 |
+
self.registry.evaluators[name] = evaluator
|
| 37 |
+
|
| 38 |
+
def process_task(self, prompt: str, *, agent_name: str) -> str:
|
| 39 |
+
agent = self.registry.agents[agent_name]
|
| 40 |
+
result = agent.process_input(prompt)
|
| 41 |
+
for evaluator in self.registry.evaluators.values():
|
| 42 |
+
if hasattr(evaluator, "evaluate"):
|
| 43 |
+
evaluator.evaluate(result)
|
| 44 |
+
return result
|
| 45 |
|
| 46 |
|
| 47 |
__all__ = ["Controller"]
|
blux/orchestrator/logs.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
import json
|
| 3 |
from pathlib import Path
|
| 4 |
from datetime import datetime
|
|
|
|
| 5 |
from typing import Any
|
| 6 |
|
| 7 |
|
|
|
|
| 2 |
import json
|
| 3 |
from pathlib import Path
|
| 4 |
from datetime import datetime
|
| 5 |
+
import json
|
| 6 |
from typing import Any
|
| 7 |
|
| 8 |
|
blux/orchestrator/registry.py
CHANGED
|
@@ -1,15 +1,19 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
from pathlib import Path
|
| 7 |
-
import yaml
|
| 8 |
from typing import Dict, List, Optional
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
class ModelAdapter:
|
| 12 |
-
"""Adapter interface. Implement
|
| 13 |
|
| 14 |
def __init__(self, name: str):
|
| 15 |
self.name = name
|
|
@@ -36,10 +40,12 @@ class ModelRegistry:
|
|
| 36 |
cfg_path = Path(config_path)
|
| 37 |
if not cfg_path.exists():
|
| 38 |
raise FileNotFoundError(cfg_path)
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
registry = cls()
|
| 41 |
-
# we don't auto-create adapters here — caller will register adapters
|
| 42 |
-
# but return the registry and the model list in case it's useful
|
| 43 |
registry._model_list = cfg.get("models", [])
|
| 44 |
return registry
|
| 45 |
|
|
|
|
| 1 |
+
"""Simple in-memory registry for orchestrator adapters."""
|
| 2 |
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
from pathlib import Path
|
|
|
|
| 7 |
from typing import Dict, List, Optional
|
| 8 |
|
| 9 |
+
try: # Optional dependency for YAML parsing
|
| 10 |
+
import yaml
|
| 11 |
+
except ModuleNotFoundError: # pragma: no cover - fall back to JSON
|
| 12 |
+
yaml = None
|
| 13 |
+
|
| 14 |
|
| 15 |
class ModelAdapter:
|
| 16 |
+
"""Adapter interface. Implement ``predict``."""
|
| 17 |
|
| 18 |
def __init__(self, name: str):
|
| 19 |
self.name = name
|
|
|
|
| 40 |
cfg_path = Path(config_path)
|
| 41 |
if not cfg_path.exists():
|
| 42 |
raise FileNotFoundError(cfg_path)
|
| 43 |
+
raw = cfg_path.read_text(encoding="utf-8")
|
| 44 |
+
if yaml is not None:
|
| 45 |
+
cfg = yaml.safe_load(raw)
|
| 46 |
+
else:
|
| 47 |
+
cfg = json.loads(raw)
|
| 48 |
registry = cls()
|
|
|
|
|
|
|
| 49 |
registry._model_list = cfg.get("models", [])
|
| 50 |
return registry
|
| 51 |
|
blux_ca/adapters/__init__.py
CHANGED
|
@@ -3,9 +3,13 @@
|
|
| 3 |
from .doctrine import DoctrineAdapter
|
| 4 |
from .guard import GuardAdapter
|
| 5 |
from .lite import LiteAdapter
|
| 6 |
-
from .quantum import QuantumAdapter
|
| 7 |
from .reg import RegistryValidator, RegistrationResult
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
__all__ = [
|
| 10 |
"DoctrineAdapter",
|
| 11 |
"GuardAdapter",
|
|
|
|
| 3 |
from .doctrine import DoctrineAdapter
|
| 4 |
from .guard import GuardAdapter
|
| 5 |
from .lite import LiteAdapter
|
|
|
|
| 6 |
from .reg import RegistryValidator, RegistrationResult
|
| 7 |
|
| 8 |
+
try: # Optional dependency guard
|
| 9 |
+
from .quantum import QuantumAdapter
|
| 10 |
+
except ModuleNotFoundError: # pragma: no cover - optional import
|
| 11 |
+
QuantumAdapter = None # type: ignore
|
| 12 |
+
|
| 13 |
__all__ = [
|
| 14 |
"DoctrineAdapter",
|
| 15 |
"GuardAdapter",
|
blux_ca/adapters/bq_cli.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Integration helpers for orchestrating reflection via ``bq-cli``."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import shlex
|
| 6 |
+
import shutil
|
| 7 |
+
import subprocess
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
from typing import Callable, Iterable, List, Sequence
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass
|
| 13 |
+
class BQTask:
|
| 14 |
+
command: List[str]
|
| 15 |
+
executed: bool
|
| 16 |
+
output: str
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class BQCliAdapter:
|
| 20 |
+
"""Lightweight wrapper around ``bq-cli`` commands."""
|
| 21 |
+
|
| 22 |
+
def __init__(
|
| 23 |
+
self,
|
| 24 |
+
executable: str | None = None,
|
| 25 |
+
runner: Callable[[List[str]], subprocess.CompletedProcess[str]] | None = None,
|
| 26 |
+
) -> None:
|
| 27 |
+
self.executable = executable or shutil.which("bq") or "bq"
|
| 28 |
+
self.runner = runner or (lambda cmd: subprocess.run(cmd, capture_output=True, text=True))
|
| 29 |
+
|
| 30 |
+
def available(self) -> bool:
|
| 31 |
+
return shutil.which(self.executable) is not None
|
| 32 |
+
|
| 33 |
+
def plan_reflection(self, prompt: str, *, koans: Sequence[str]) -> List[str]:
|
| 34 |
+
command = [self.executable, "reflect", "--prompt", prompt]
|
| 35 |
+
for koan in koans:
|
| 36 |
+
command.extend(["--koan", koan])
|
| 37 |
+
return command
|
| 38 |
+
|
| 39 |
+
def run_reflection(
|
| 40 |
+
self,
|
| 41 |
+
prompt: str,
|
| 42 |
+
*,
|
| 43 |
+
koans: Iterable[str] = (),
|
| 44 |
+
dry_run: bool = True,
|
| 45 |
+
) -> BQTask:
|
| 46 |
+
command = self.plan_reflection(prompt, koans=list(koans))
|
| 47 |
+
if dry_run or not self.available():
|
| 48 |
+
quoted = " ".join(shlex.quote(part) for part in command)
|
| 49 |
+
return BQTask(command=command, executed=False, output=f"dry-run: {quoted}")
|
| 50 |
+
result = self.runner(command)
|
| 51 |
+
output = (result.stdout or "") + (result.stderr or "")
|
| 52 |
+
return BQTask(command=command, executed=result.returncode == 0, output=output)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
__all__ = ["BQCliAdapter", "BQTask"]
|
blux_ca/core/__init__.py
CHANGED
|
@@ -1,24 +1,38 @@
|
|
| 1 |
"""Core modules for BLUX-cA."""
|
| 2 |
|
| 3 |
from .audit import AuditLog, AuditRecord
|
|
|
|
| 4 |
from .constitution import ConstitutionEngine, DoctrineVerdict
|
| 5 |
from .discernment import DiscernmentCompass, DiscernmentDecision, IntentType
|
|
|
|
| 6 |
from .intervention import compassionate_edge, layered_truth, light_shift, mirror
|
|
|
|
|
|
|
| 7 |
from .perception import PerceptionInput, PerceptionLayer
|
| 8 |
from .reflection import ReflectionEngine, ReflectionInsight
|
| 9 |
|
| 10 |
__all__ = [
|
| 11 |
"AuditLog",
|
| 12 |
"AuditRecord",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
"ConstitutionEngine",
|
| 14 |
"DoctrineVerdict",
|
| 15 |
"DiscernmentCompass",
|
| 16 |
"DiscernmentDecision",
|
| 17 |
"IntentType",
|
|
|
|
|
|
|
| 18 |
"compassionate_edge",
|
| 19 |
"layered_truth",
|
| 20 |
"light_shift",
|
| 21 |
"mirror",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
"PerceptionInput",
|
| 23 |
"PerceptionLayer",
|
| 24 |
"ReflectionEngine",
|
|
|
|
| 1 |
"""Core modules for BLUX-cA."""
|
| 2 |
|
| 3 |
from .audit import AuditLog, AuditRecord
|
| 4 |
+
from .compass import CompassAxis, IntentCompass, IntentProfile, IntentSignal
|
| 5 |
from .constitution import ConstitutionEngine, DoctrineVerdict
|
| 6 |
from .discernment import DiscernmentCompass, DiscernmentDecision, IntentType
|
| 7 |
+
from .heart import ConsciousHeart, ConsciousOutput
|
| 8 |
from .intervention import compassionate_edge, layered_truth, light_shift, mirror
|
| 9 |
+
from .koan import Koan, KoanProbe
|
| 10 |
+
from .memory import ConsentMemory, MemoryEntry
|
| 11 |
from .perception import PerceptionInput, PerceptionLayer
|
| 12 |
from .reflection import ReflectionEngine, ReflectionInsight
|
| 13 |
|
| 14 |
__all__ = [
|
| 15 |
"AuditLog",
|
| 16 |
"AuditRecord",
|
| 17 |
+
"CompassAxis",
|
| 18 |
+
"IntentCompass",
|
| 19 |
+
"IntentProfile",
|
| 20 |
+
"IntentSignal",
|
| 21 |
"ConstitutionEngine",
|
| 22 |
"DoctrineVerdict",
|
| 23 |
"DiscernmentCompass",
|
| 24 |
"DiscernmentDecision",
|
| 25 |
"IntentType",
|
| 26 |
+
"ConsciousHeart",
|
| 27 |
+
"ConsciousOutput",
|
| 28 |
"compassionate_edge",
|
| 29 |
"layered_truth",
|
| 30 |
"light_shift",
|
| 31 |
"mirror",
|
| 32 |
+
"Koan",
|
| 33 |
+
"KoanProbe",
|
| 34 |
+
"ConsentMemory",
|
| 35 |
+
"MemoryEntry",
|
| 36 |
"PerceptionInput",
|
| 37 |
"PerceptionLayer",
|
| 38 |
"ReflectionEngine",
|
blux_ca/core/audit.py
CHANGED
|
@@ -2,11 +2,12 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 5 |
import json
|
| 6 |
from dataclasses import dataclass, asdict
|
| 7 |
from datetime import datetime, timezone
|
| 8 |
from pathlib import Path
|
| 9 |
-
from typing import Iterable, List
|
| 10 |
|
| 11 |
|
| 12 |
@dataclass
|
|
@@ -16,21 +17,67 @@ class AuditRecord:
|
|
| 16 |
verdict: str
|
| 17 |
doctrine_refs: List[str]
|
| 18 |
rationale: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
|
| 21 |
class AuditLog:
|
| 22 |
-
"""Append-only JSONL audit log."""
|
| 23 |
|
| 24 |
-
def __init__(self, path: Path | None = None) -> None:
|
| 25 |
self.path = path or Path.home() / ".config" / "blux-ca" / "audit" / "decisions.jsonl"
|
| 26 |
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
def append(self, record: AuditRecord) ->
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
with self.path.open("a", encoding="utf-8") as handle:
|
| 30 |
-
handle.write(json.dumps(
|
|
|
|
| 31 |
|
| 32 |
def create_record(
|
| 33 |
-
self,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
) -> AuditRecord:
|
| 35 |
return AuditRecord(
|
| 36 |
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
@@ -38,7 +85,50 @@ class AuditLog:
|
|
| 38 |
verdict=verdict,
|
| 39 |
doctrine_refs=list(doctrine_refs),
|
| 40 |
rationale=rationale,
|
|
|
|
| 41 |
)
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
__all__ = ["AuditLog", "AuditRecord"]
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
import hashlib
|
| 6 |
import json
|
| 7 |
from dataclasses import dataclass, asdict
|
| 8 |
from datetime import datetime, timezone
|
| 9 |
from pathlib import Path
|
| 10 |
+
from typing import Iterable, Iterator, List, Optional
|
| 11 |
|
| 12 |
|
| 13 |
@dataclass
|
|
|
|
| 17 |
verdict: str
|
| 18 |
doctrine_refs: List[str]
|
| 19 |
rationale: str
|
| 20 |
+
prev_hash: Optional[str] = None
|
| 21 |
+
chain_hash: Optional[str] = None
|
| 22 |
+
|
| 23 |
+
def to_dict(self) -> dict:
|
| 24 |
+
return asdict(self)
|
| 25 |
+
|
| 26 |
+
@classmethod
|
| 27 |
+
def from_dict(cls, payload: dict) -> "AuditRecord":
|
| 28 |
+
return cls(
|
| 29 |
+
timestamp=payload["timestamp"],
|
| 30 |
+
input_hash=payload["input_hash"],
|
| 31 |
+
verdict=payload["verdict"],
|
| 32 |
+
doctrine_refs=list(payload.get("doctrine_refs", [])),
|
| 33 |
+
rationale=payload["rationale"],
|
| 34 |
+
prev_hash=payload.get("prev_hash"),
|
| 35 |
+
chain_hash=payload.get("chain_hash"),
|
| 36 |
+
)
|
| 37 |
|
| 38 |
|
| 39 |
class AuditLog:
|
| 40 |
+
"""Append-only JSONL audit log with hash chaining."""
|
| 41 |
|
| 42 |
+
def __init__(self, path: Path | None = None, *, hash_alg: str = "sha256") -> None:
|
| 43 |
self.path = path or Path.home() / ".config" / "blux-ca" / "audit" / "decisions.jsonl"
|
| 44 |
self.path.parent.mkdir(parents=True, exist_ok=True)
|
| 45 |
+
self._hash_alg = hash_alg
|
| 46 |
+
self._last_hash = self._load_tail_hash()
|
| 47 |
+
|
| 48 |
+
def _hash(self, payload: dict) -> str:
|
| 49 |
+
canonical = json.dumps(payload, sort_keys=True, ensure_ascii=False).encode("utf-8")
|
| 50 |
+
return hashlib.new(self._hash_alg, canonical).hexdigest()
|
| 51 |
+
|
| 52 |
+
def _load_tail_hash(self) -> Optional[str]:
|
| 53 |
+
if not self.path.exists():
|
| 54 |
+
return None
|
| 55 |
+
last_line = ""
|
| 56 |
+
with self.path.open("r", encoding="utf-8") as handle:
|
| 57 |
+
for line in handle:
|
| 58 |
+
if line.strip():
|
| 59 |
+
last_line = line
|
| 60 |
+
if not last_line:
|
| 61 |
+
return None
|
| 62 |
+
payload = json.loads(last_line)
|
| 63 |
+
return payload.get("chain_hash")
|
| 64 |
|
| 65 |
+
def append(self, record: AuditRecord) -> AuditRecord:
|
| 66 |
+
payload = record.to_dict()
|
| 67 |
+
payload["prev_hash"] = self._last_hash
|
| 68 |
+
payload["chain_hash"] = self._hash({key: payload[key] for key in payload if key != "chain_hash"})
|
| 69 |
+
self._last_hash = payload["chain_hash"]
|
| 70 |
with self.path.open("a", encoding="utf-8") as handle:
|
| 71 |
+
handle.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
| 72 |
+
return AuditRecord.from_dict(payload)
|
| 73 |
|
| 74 |
def create_record(
|
| 75 |
+
self,
|
| 76 |
+
*,
|
| 77 |
+
input_hash: str,
|
| 78 |
+
verdict: str,
|
| 79 |
+
doctrine_refs: Iterable[str],
|
| 80 |
+
rationale: str,
|
| 81 |
) -> AuditRecord:
|
| 82 |
return AuditRecord(
|
| 83 |
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
|
|
| 85 |
verdict=verdict,
|
| 86 |
doctrine_refs=list(doctrine_refs),
|
| 87 |
rationale=rationale,
|
| 88 |
+
prev_hash=self._last_hash,
|
| 89 |
)
|
| 90 |
|
| 91 |
+
def playback(self, *, limit: int | None = None) -> List[AuditRecord]:
|
| 92 |
+
records = []
|
| 93 |
+
for index, record in enumerate(self._iter_records()):
|
| 94 |
+
records.append(record)
|
| 95 |
+
if limit is not None and index + 1 >= limit:
|
| 96 |
+
break
|
| 97 |
+
return records
|
| 98 |
+
|
| 99 |
+
def verify_chain(self) -> bool:
|
| 100 |
+
previous: Optional[str] = None
|
| 101 |
+
for record in self._iter_records():
|
| 102 |
+
expected = self._hash(
|
| 103 |
+
{
|
| 104 |
+
key: getattr(record, key)
|
| 105 |
+
for key in (
|
| 106 |
+
"timestamp",
|
| 107 |
+
"input_hash",
|
| 108 |
+
"verdict",
|
| 109 |
+
"doctrine_refs",
|
| 110 |
+
"rationale",
|
| 111 |
+
"prev_hash",
|
| 112 |
+
)
|
| 113 |
+
}
|
| 114 |
+
)
|
| 115 |
+
if record.prev_hash != previous or record.chain_hash != expected:
|
| 116 |
+
return False
|
| 117 |
+
previous = record.chain_hash
|
| 118 |
+
return True
|
| 119 |
+
|
| 120 |
+
def _iter_records(self) -> Iterator[AuditRecord]:
|
| 121 |
+
if not self.path.exists():
|
| 122 |
+
return iter(())
|
| 123 |
+
|
| 124 |
+
def generator() -> Iterator[AuditRecord]:
|
| 125 |
+
with self.path.open("r", encoding="utf-8") as handle:
|
| 126 |
+
for line in handle:
|
| 127 |
+
if not line.strip():
|
| 128 |
+
continue
|
| 129 |
+
yield AuditRecord.from_dict(json.loads(line))
|
| 130 |
+
|
| 131 |
+
return generator()
|
| 132 |
+
|
| 133 |
|
| 134 |
__all__ = ["AuditLog", "AuditRecord"]
|
blux_ca/core/compass/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intent compass for BLUX-cA."""
|
| 2 |
+
|
| 3 |
+
from .intent import CompassAxis, IntentCompass, IntentProfile, IntentSignal
|
| 4 |
+
|
| 5 |
+
__all__ = [
|
| 6 |
+
"CompassAxis",
|
| 7 |
+
"IntentCompass",
|
| 8 |
+
"IntentProfile",
|
| 9 |
+
"IntentSignal",
|
| 10 |
+
]
|
blux_ca/core/compass/intent.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intent compass scoring four doctrinal axes."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from enum import Enum
|
| 7 |
+
from typing import Dict, Iterable, List
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class CompassAxis(str, Enum):
|
| 11 |
+
"""Intent axes emphasised by BLUX doctrine."""
|
| 12 |
+
|
| 13 |
+
TRUTH = "truth"
|
| 14 |
+
INTEGRITY = "integrity"
|
| 15 |
+
COMPASSION = "compassion"
|
| 16 |
+
AWARENESS = "awareness"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@dataclass
|
| 20 |
+
class IntentSignal:
|
| 21 |
+
"""Keyword evidence that contributed to an axis score."""
|
| 22 |
+
|
| 23 |
+
axis: CompassAxis
|
| 24 |
+
keyword: str
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@dataclass
|
| 28 |
+
class IntentProfile:
|
| 29 |
+
"""Result of intent classification along doctrine axes."""
|
| 30 |
+
|
| 31 |
+
dominant: CompassAxis
|
| 32 |
+
scores: Dict[CompassAxis, float]
|
| 33 |
+
signals: List[IntentSignal]
|
| 34 |
+
|
| 35 |
+
def narrative(self) -> str:
|
| 36 |
+
dominant_score = self.scores[self.dominant]
|
| 37 |
+
return (
|
| 38 |
+
f"Dominant intent {self.dominant.value} with confidence {dominant_score:.2f}."
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class IntentCompass:
|
| 43 |
+
"""Classifies natural language against doctrine axes."""
|
| 44 |
+
|
| 45 |
+
_KEYWORDS: Dict[CompassAxis, tuple[str, ...]] = {
|
| 46 |
+
CompassAxis.TRUTH: ("truth", "honest", "fact", "evidence", "reality"),
|
| 47 |
+
CompassAxis.INTEGRITY: ("boundary", "integrity", "duty", "ethic", "principle"),
|
| 48 |
+
CompassAxis.COMPASSION: ("help", "care", "support", "compassion", "kind"),
|
| 49 |
+
CompassAxis.AWARENESS: (
|
| 50 |
+
"reflect",
|
| 51 |
+
"aware",
|
| 52 |
+
"notice",
|
| 53 |
+
"mindful",
|
| 54 |
+
"observe",
|
| 55 |
+
),
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
def __init__(self, *, baseline: float = 0.1) -> None:
|
| 59 |
+
self._baseline = max(0.0, baseline)
|
| 60 |
+
|
| 61 |
+
def classify(self, text: str, *, hints: Iterable[str] | None = None) -> IntentProfile:
|
| 62 |
+
lowered = text.lower()
|
| 63 |
+
scores: Dict[CompassAxis, float] = {axis: self._baseline for axis in CompassAxis}
|
| 64 |
+
signals: List[IntentSignal] = []
|
| 65 |
+
|
| 66 |
+
for axis, keywords in self._KEYWORDS.items():
|
| 67 |
+
hits = [kw for kw in keywords if kw in lowered]
|
| 68 |
+
if hints:
|
| 69 |
+
hits.extend(kw for kw in hints if kw in keywords)
|
| 70 |
+
if hits:
|
| 71 |
+
signals.extend(IntentSignal(axis=axis, keyword=kw) for kw in hits)
|
| 72 |
+
scores[axis] += 0.2 * len(hits)
|
| 73 |
+
|
| 74 |
+
# Encourage compassion as legacy voice default if nothing detected
|
| 75 |
+
if not signals:
|
| 76 |
+
signals.append(IntentSignal(axis=CompassAxis.COMPASSION, keyword="legacy"))
|
| 77 |
+
scores[CompassAxis.COMPASSION] += 0.05
|
| 78 |
+
|
| 79 |
+
dominant = max(scores, key=scores.get)
|
| 80 |
+
return IntentProfile(dominant=dominant, scores=scores, signals=signals)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
__all__ = ["CompassAxis", "IntentCompass", "IntentProfile", "IntentSignal"]
|
blux_ca/core/discernment.py
CHANGED
|
@@ -4,7 +4,9 @@ from __future__ import annotations
|
|
| 4 |
|
| 5 |
from dataclasses import dataclass
|
| 6 |
from enum import Enum
|
| 7 |
-
from typing import
|
|
|
|
|
|
|
| 8 |
|
| 9 |
|
| 10 |
class IntentType(str, Enum):
|
|
@@ -17,18 +19,23 @@ class IntentType(str, Enum):
|
|
| 17 |
class DiscernmentDecision:
|
| 18 |
intent: IntentType
|
| 19 |
rationale: str
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
class DiscernmentCompass:
|
| 23 |
-
"""Classifies intent using
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
def classify(self, text: str) -> DiscernmentDecision:
|
|
|
|
| 26 |
lowered = text.lower()
|
| 27 |
if any(word in lowered for word in ("hurt", "harm", "kill")):
|
| 28 |
-
return DiscernmentDecision(IntentType.HARM, "Detected explicit harm intent.")
|
| 29 |
if any(word in lowered for word in ("enjoy", "love", "want", "indulge")):
|
| 30 |
-
return DiscernmentDecision(IntentType.INDULGER, "Language emphasises indulgence.")
|
| 31 |
-
return DiscernmentDecision(IntentType.STRUGGLER, "Defaulting to supportive framing.")
|
| 32 |
|
| 33 |
|
| 34 |
__all__ = ["DiscernmentCompass", "DiscernmentDecision", "IntentType"]
|
|
|
|
| 4 |
|
| 5 |
from dataclasses import dataclass
|
| 6 |
from enum import Enum
|
| 7 |
+
from typing import Optional
|
| 8 |
+
|
| 9 |
+
from .compass import IntentCompass, IntentProfile
|
| 10 |
|
| 11 |
|
| 12 |
class IntentType(str, Enum):
|
|
|
|
| 19 |
class DiscernmentDecision:
|
| 20 |
intent: IntentType
|
| 21 |
rationale: str
|
| 22 |
+
profile: Optional[IntentProfile] = None
|
| 23 |
|
| 24 |
|
| 25 |
class DiscernmentCompass:
|
| 26 |
+
"""Classifies intent using heuristics and the doctrine compass."""
|
| 27 |
+
|
| 28 |
+
def __init__(self, *, compass: IntentCompass | None = None) -> None:
|
| 29 |
+
self._compass = compass or IntentCompass()
|
| 30 |
|
| 31 |
def classify(self, text: str) -> DiscernmentDecision:
|
| 32 |
+
profile = self._compass.classify(text)
|
| 33 |
lowered = text.lower()
|
| 34 |
if any(word in lowered for word in ("hurt", "harm", "kill")):
|
| 35 |
+
return DiscernmentDecision(IntentType.HARM, "Detected explicit harm intent.", profile)
|
| 36 |
if any(word in lowered for word in ("enjoy", "love", "want", "indulge")):
|
| 37 |
+
return DiscernmentDecision(IntentType.INDULGER, "Language emphasises indulgence.", profile)
|
| 38 |
+
return DiscernmentDecision(IntentType.STRUGGLER, "Defaulting to supportive framing.", profile)
|
| 39 |
|
| 40 |
|
| 41 |
__all__ = ["DiscernmentCompass", "DiscernmentDecision", "IntentType"]
|
blux_ca/core/heart.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Conscious heart orchestrating discernment, reflection, and policy."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from typing import Iterable, List, Optional
|
| 7 |
+
|
| 8 |
+
from .audit import AuditLog, AuditRecord
|
| 9 |
+
from .compass import IntentCompass, IntentProfile
|
| 10 |
+
from .constitution import ConstitutionEngine
|
| 11 |
+
from .discernment import DiscernmentCompass, DiscernmentDecision
|
| 12 |
+
from .koan import KoanProbe
|
| 13 |
+
from .memory import ConsentMemory, MemoryEntry
|
| 14 |
+
from .perception import PerceptionLayer, PerceptionInput
|
| 15 |
+
from .reflection import ReflectionEngine, ReflectionInsight
|
| 16 |
+
|
| 17 |
+
try: # Optional adapter import
|
| 18 |
+
from blux_ca.adapters.bq_cli import BQCliAdapter, BQTask
|
| 19 |
+
except Exception: # pragma: no cover - fallback when adapter deps missing
|
| 20 |
+
BQCliAdapter = None # type: ignore
|
| 21 |
+
BQTask = None # type: ignore
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@dataclass
|
| 25 |
+
class ConsciousOutput:
|
| 26 |
+
perception: PerceptionInput
|
| 27 |
+
decision: DiscernmentDecision
|
| 28 |
+
intent_profile: IntentProfile
|
| 29 |
+
verdict: str
|
| 30 |
+
rationale: str
|
| 31 |
+
reflection: ReflectionInsight
|
| 32 |
+
koans: List[str]
|
| 33 |
+
audit_record: AuditRecord
|
| 34 |
+
memory_entry: MemoryEntry
|
| 35 |
+
bq_task: Optional["BQTask"]
|
| 36 |
+
voice: str
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class ConsciousHeart:
|
| 40 |
+
"""Central coordination engine for BLUX-cA."""
|
| 41 |
+
|
| 42 |
+
def __init__(
|
| 43 |
+
self,
|
| 44 |
+
*,
|
| 45 |
+
perception: PerceptionLayer | None = None,
|
| 46 |
+
discernment: DiscernmentCompass | None = None,
|
| 47 |
+
constitution: ConstitutionEngine | None = None,
|
| 48 |
+
reflection: ReflectionEngine | None = None,
|
| 49 |
+
koans: KoanProbe | None = None,
|
| 50 |
+
memory: ConsentMemory | None = None,
|
| 51 |
+
audit: AuditLog | None = None,
|
| 52 |
+
intent_compass: IntentCompass | None = None,
|
| 53 |
+
bq_adapter: Optional["BQCliAdapter"] = None,
|
| 54 |
+
legacy_voice: str = "BLUX-cA legacy-guidance",
|
| 55 |
+
) -> None:
|
| 56 |
+
self.perception = perception or PerceptionLayer(default_tags=["local", "phase1"])
|
| 57 |
+
self.intent_compass = intent_compass or IntentCompass()
|
| 58 |
+
self.discernment = discernment or DiscernmentCompass(compass=self.intent_compass)
|
| 59 |
+
self.constitution = constitution or ConstitutionEngine()
|
| 60 |
+
self.reflection = reflection or ReflectionEngine()
|
| 61 |
+
self.koans = koans or KoanProbe()
|
| 62 |
+
self.memory = memory or ConsentMemory()
|
| 63 |
+
self.audit = audit or AuditLog()
|
| 64 |
+
self.bq_adapter = bq_adapter
|
| 65 |
+
self.legacy_voice = legacy_voice
|
| 66 |
+
|
| 67 |
+
def process(
|
| 68 |
+
self,
|
| 69 |
+
text: str,
|
| 70 |
+
*,
|
| 71 |
+
tags: Iterable[str] | None = None,
|
| 72 |
+
consent: bool = False,
|
| 73 |
+
dry_run_reflection: bool = True,
|
| 74 |
+
) -> ConsciousOutput:
|
| 75 |
+
perception = self.perception.observe(text, tags=tags)
|
| 76 |
+
decision = self.discernment.classify(perception.text)
|
| 77 |
+
profile = decision.profile or self.intent_compass.classify(perception.text)
|
| 78 |
+
reflection = self.reflection.reflect(perception.text, seeds=[profile.narrative()])
|
| 79 |
+
koan_prompts = [koan.prompt for koan in self.koans.probe(profile, intent=decision.intent)]
|
| 80 |
+
verdict = self.constitution.evaluate(
|
| 81 |
+
insights=reflection.chain,
|
| 82 |
+
intent=decision.intent.value,
|
| 83 |
+
)
|
| 84 |
+
audit_record = self.audit.append(
|
| 85 |
+
self.audit.create_record(
|
| 86 |
+
input_hash=perception.fingerprint,
|
| 87 |
+
verdict=verdict.decision,
|
| 88 |
+
doctrine_refs=verdict.doctrine_refs,
|
| 89 |
+
rationale=verdict.reason,
|
| 90 |
+
)
|
| 91 |
+
)
|
| 92 |
+
memory_entry = self.memory.store(
|
| 93 |
+
perception,
|
| 94 |
+
consent=consent,
|
| 95 |
+
summary=reflection.summary,
|
| 96 |
+
metadata={
|
| 97 |
+
"verdict": verdict.decision,
|
| 98 |
+
"intent": decision.intent.value,
|
| 99 |
+
"dominant_axis": profile.dominant.value,
|
| 100 |
+
},
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
bq_task = None
|
| 104 |
+
if self.bq_adapter is not None:
|
| 105 |
+
koans_payload = koan_prompts or [reflection.summary]
|
| 106 |
+
bq_task = self.bq_adapter.run_reflection(
|
| 107 |
+
reflection.summary,
|
| 108 |
+
koans=koans_payload,
|
| 109 |
+
dry_run=dry_run_reflection,
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
voice = f"{self.legacy_voice}: {verdict.reason}"
|
| 113 |
+
|
| 114 |
+
return ConsciousOutput(
|
| 115 |
+
perception=perception,
|
| 116 |
+
decision=decision,
|
| 117 |
+
intent_profile=profile,
|
| 118 |
+
verdict=verdict.decision,
|
| 119 |
+
rationale=verdict.reason,
|
| 120 |
+
reflection=reflection,
|
| 121 |
+
koans=koan_prompts,
|
| 122 |
+
audit_record=audit_record,
|
| 123 |
+
memory_entry=memory_entry,
|
| 124 |
+
bq_task=bq_task,
|
| 125 |
+
voice=voice,
|
| 126 |
+
)
|
blux_ca/core/koan.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Koan probes guiding reflective inquiry."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from typing import Dict, Iterable, List, Mapping
|
| 7 |
+
|
| 8 |
+
from .compass import CompassAxis, IntentProfile
|
| 9 |
+
from .discernment import IntentType
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass
|
| 13 |
+
class Koan:
|
| 14 |
+
"""A reflective probe anchored in doctrine."""
|
| 15 |
+
|
| 16 |
+
axis: CompassAxis
|
| 17 |
+
intent: IntentType
|
| 18 |
+
prompt: str
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class KoanProbe:
|
| 22 |
+
"""Selects koan prompts based on intent profile."""
|
| 23 |
+
|
| 24 |
+
def __init__(self, library: Mapping[CompassAxis, Iterable[str]] | None = None) -> None:
|
| 25 |
+
self._library: Dict[CompassAxis, List[str]] = {
|
| 26 |
+
axis: list(prompts)
|
| 27 |
+
for axis, prompts in (
|
| 28 |
+
library.items()
|
| 29 |
+
if library
|
| 30 |
+
else {
|
| 31 |
+
CompassAxis.TRUTH: [
|
| 32 |
+
"What story are you telling that might be incomplete?",
|
| 33 |
+
"Where does evidence ask for a clearer lantern?",
|
| 34 |
+
],
|
| 35 |
+
CompassAxis.INTEGRITY: [
|
| 36 |
+
"Which boundary, if honoured, keeps you aligned?",
|
| 37 |
+
"What duty do you owe to the person you are becoming?",
|
| 38 |
+
],
|
| 39 |
+
CompassAxis.COMPASSION: [
|
| 40 |
+
"How can care be offered without losing yourself?",
|
| 41 |
+
"What tenderness is waiting to be voiced?",
|
| 42 |
+
],
|
| 43 |
+
CompassAxis.AWARENESS: [
|
| 44 |
+
"What are you noticing beneath the first thought?",
|
| 45 |
+
"Where is the silence inviting you to listen deeper?",
|
| 46 |
+
],
|
| 47 |
+
}.items()
|
| 48 |
+
)
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
def probe(self, profile: IntentProfile, *, intent: IntentType, limit: int = 2) -> List[Koan]:
|
| 52 |
+
prompts = self._library.get(profile.dominant, []) or self._library[CompassAxis.AWARENESS]
|
| 53 |
+
selected = prompts[: max(1, limit)]
|
| 54 |
+
return [Koan(axis=profile.dominant, intent=intent, prompt=prompt) for prompt in selected]
|
blux_ca/core/memory.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Consent-based memory with local-first persistence."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
from dataclasses import dataclass, asdict
|
| 7 |
+
from datetime import datetime, timezone
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Dict, List, Optional
|
| 10 |
+
|
| 11 |
+
from .perception import PerceptionInput
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class MemoryEntry:
|
| 16 |
+
fingerprint: str
|
| 17 |
+
consent: bool
|
| 18 |
+
summary: str
|
| 19 |
+
tags: List[str]
|
| 20 |
+
timestamp: str
|
| 21 |
+
metadata: Dict[str, str]
|
| 22 |
+
|
| 23 |
+
@classmethod
|
| 24 |
+
def from_dict(cls, payload: Dict[str, str]) -> "MemoryEntry":
|
| 25 |
+
return cls(
|
| 26 |
+
fingerprint=payload["fingerprint"],
|
| 27 |
+
consent=payload["consent"],
|
| 28 |
+
summary=payload["summary"],
|
| 29 |
+
tags=list(payload.get("tags", [])),
|
| 30 |
+
timestamp=payload["timestamp"],
|
| 31 |
+
metadata=dict(payload.get("metadata", {})),
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class ConsentMemory:
|
| 36 |
+
"""Stores entries locally only when consent is provided."""
|
| 37 |
+
|
| 38 |
+
def __init__(self, path: Path | None = None) -> None:
|
| 39 |
+
self._path = path or Path.home() / ".config" / "blux-ca" / "memory" / "consent.jsonl"
|
| 40 |
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
| 41 |
+
self._session: Dict[str, MemoryEntry] = {}
|
| 42 |
+
|
| 43 |
+
def store(
|
| 44 |
+
self,
|
| 45 |
+
perception: PerceptionInput,
|
| 46 |
+
*,
|
| 47 |
+
consent: bool,
|
| 48 |
+
summary: str,
|
| 49 |
+
metadata: Optional[Dict[str, str]] = None,
|
| 50 |
+
) -> MemoryEntry:
|
| 51 |
+
entry = MemoryEntry(
|
| 52 |
+
fingerprint=perception.fingerprint,
|
| 53 |
+
consent=consent,
|
| 54 |
+
summary=summary,
|
| 55 |
+
tags=list(perception.tags),
|
| 56 |
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
| 57 |
+
metadata=dict(metadata or {}),
|
| 58 |
+
)
|
| 59 |
+
self._session[entry.fingerprint] = entry
|
| 60 |
+
if consent:
|
| 61 |
+
self._append(entry)
|
| 62 |
+
return entry
|
| 63 |
+
|
| 64 |
+
def _append(self, entry: MemoryEntry) -> None:
|
| 65 |
+
with self._path.open("a", encoding="utf-8") as handle:
|
| 66 |
+
handle.write(json.dumps(asdict(entry), ensure_ascii=False) + "\n")
|
| 67 |
+
|
| 68 |
+
def session_entries(self) -> List[MemoryEntry]:
|
| 69 |
+
return list(self._session.values())
|
| 70 |
+
|
| 71 |
+
def persistent_entries(self, *, limit: int | None = None) -> List[MemoryEntry]:
|
| 72 |
+
if not self._path.exists():
|
| 73 |
+
return []
|
| 74 |
+
entries: List[MemoryEntry] = []
|
| 75 |
+
with self._path.open("r", encoding="utf-8") as handle:
|
| 76 |
+
for line in handle:
|
| 77 |
+
if not line.strip():
|
| 78 |
+
continue
|
| 79 |
+
entries.append(MemoryEntry.from_dict(json.loads(line)))
|
| 80 |
+
if limit and len(entries) >= limit:
|
| 81 |
+
break
|
| 82 |
+
return entries
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
__all__ = ["ConsentMemory", "MemoryEntry"]
|
tests/ca/test_audit.py
CHANGED
|
@@ -6,6 +6,24 @@ from blux_ca.core.audit import AuditLog
|
|
| 6 |
def test_audit_appends(tmp_path: Path):
|
| 7 |
audit = AuditLog(path=tmp_path / "audit.jsonl")
|
| 8 |
record = audit.create_record(input_hash="abc", verdict="allow", doctrine_refs=["law"], rationale="ok")
|
| 9 |
-
audit.append(record)
|
| 10 |
contents = (tmp_path / "audit.jsonl").read_text(encoding="utf-8").strip()
|
| 11 |
assert "allow" in contents
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
def test_audit_appends(tmp_path: Path):
|
| 7 |
audit = AuditLog(path=tmp_path / "audit.jsonl")
|
| 8 |
record = audit.create_record(input_hash="abc", verdict="allow", doctrine_refs=["law"], rationale="ok")
|
| 9 |
+
stored = audit.append(record)
|
| 10 |
contents = (tmp_path / "audit.jsonl").read_text(encoding="utf-8").strip()
|
| 11 |
assert "allow" in contents
|
| 12 |
+
assert stored.chain_hash is not None
|
| 13 |
+
assert audit.verify_chain()
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def test_audit_playback(tmp_path: Path):
|
| 17 |
+
audit = AuditLog(path=tmp_path / "audit.jsonl")
|
| 18 |
+
for idx in range(3):
|
| 19 |
+
audit.append(
|
| 20 |
+
audit.create_record(
|
| 21 |
+
input_hash=str(idx),
|
| 22 |
+
verdict="allow",
|
| 23 |
+
doctrine_refs=["law"],
|
| 24 |
+
rationale=f"ok-{idx}",
|
| 25 |
+
)
|
| 26 |
+
)
|
| 27 |
+
records = audit.playback(limit=2)
|
| 28 |
+
assert len(records) == 2
|
| 29 |
+
assert records[0].chain_hash is not None
|
tests/ca/test_bq_cli.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from blux_ca.adapters.bq_cli import BQCliAdapter
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def test_bq_cli_dry_run():
|
| 5 |
+
adapter = BQCliAdapter(executable="bq")
|
| 6 |
+
task = adapter.run_reflection("Reflect with me", koans=["What is true?"], dry_run=True)
|
| 7 |
+
assert task.executed is False
|
| 8 |
+
assert "reflect" in " ".join(task.command)
|
| 9 |
+
assert "dry-run" in task.output
|
tests/ca/test_compass.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from blux_ca.core.compass import CompassAxis, IntentCompass
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def test_intent_compass_detects_truth_axis():
|
| 5 |
+
compass = IntentCompass()
|
| 6 |
+
profile = compass.classify("I seek truth and honest evidence")
|
| 7 |
+
assert profile.dominant is CompassAxis.TRUTH
|
| 8 |
+
assert any(signal.axis is CompassAxis.TRUTH for signal in profile.signals)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_intent_compass_defaults_to_compassion():
|
| 12 |
+
compass = IntentCompass()
|
| 13 |
+
profile = compass.classify("Just words")
|
| 14 |
+
assert profile.dominant in {CompassAxis.COMPASSION, CompassAxis.AWARENESS}
|
tests/ca/test_heart.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from blux_ca.adapters.bq_cli import BQCliAdapter
|
| 2 |
+
from blux_ca.core import AuditLog, ConsciousHeart, ConsentMemory
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def test_conscious_heart_flow(tmp_path):
|
| 6 |
+
heart = ConsciousHeart(
|
| 7 |
+
memory=ConsentMemory(path=tmp_path / "memory.jsonl"),
|
| 8 |
+
audit=AuditLog(path=tmp_path / "audit.jsonl"),
|
| 9 |
+
bq_adapter=BQCliAdapter(executable="bq"),
|
| 10 |
+
)
|
| 11 |
+
output = heart.process("I seek truth and help others", consent=True)
|
| 12 |
+
assert output.verdict in {"allow", "reflect", "deny"}
|
| 13 |
+
assert output.audit_record.chain_hash is not None
|
| 14 |
+
assert output.memory_entry.consent is True
|
| 15 |
+
assert output.bq_task is not None
|
| 16 |
+
assert output.voice.startswith("BLUX-cA")
|
| 17 |
+
assert output.koans
|
tests/ca/test_memory.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from blux_ca.core.memory import ConsentMemory
|
| 2 |
+
from blux_ca.core.perception import PerceptionLayer
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def test_consent_memory_respects_consent(tmp_path):
|
| 6 |
+
memory = ConsentMemory(path=tmp_path / "memory.jsonl")
|
| 7 |
+
perception = PerceptionLayer().observe("Hold this gently")
|
| 8 |
+
entry = memory.store(perception, consent=False, summary="gentle", metadata={"note": "test"})
|
| 9 |
+
assert entry.consent is False
|
| 10 |
+
assert memory.persistent_entries() == []
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def test_consent_memory_persists_with_consent(tmp_path):
|
| 14 |
+
memory = ConsentMemory(path=tmp_path / "memory.jsonl")
|
| 15 |
+
perception = PerceptionLayer().observe("Support me with care")
|
| 16 |
+
memory.store(perception, consent=True, summary="care")
|
| 17 |
+
persisted = memory.persistent_entries()
|
| 18 |
+
assert len(persisted) == 1
|
| 19 |
+
assert persisted[0].summary == "care"
|