Spaces:
Sleeping
Sleeping
Add Phase 6 components and complete model suite for production demo
Browse files- Phase 6 components: query_classifier, semantic_tension, specialization_tracker, preflight_predictor
- All 8 LoRA adapters (Newton, DaVinci, Quantum, Philosophy, Empathy, Consciousness, Systems Architecture, Multi-Perspective)
- Complete training checkpoints for model development
- Updated .gitattributes for proper LFS tracking of .gguf files
This brings Codette-Demo to feature parity with training-lab for inference and demo use cases.
- .gitattributes +2 -0
- adapters/.gitkeep +0 -0
- adapters/consciousness-lora-f16.gguf +3 -0
- adapters/convert_peft_to_gguf.py +207 -0
- adapters/davinci-lora-f16.gguf +3 -0
- adapters/empathy-lora-f16.gguf +3 -0
- adapters/multi_perspective-lora-f16.gguf +3 -0
- adapters/newton-lora-f16.gguf +3 -0
- adapters/philosophy-lora-f16.gguf +3 -0
- adapters/quantum-lora-f16.gguf +3 -0
- adapters/systems_architecture-lora-f16.gguf +3 -0
- reasoning_forge/framework_definitions.py +211 -0
- reasoning_forge/preflight_predictor.py +347 -0
- reasoning_forge/query_classifier.py +231 -0
- reasoning_forge/semantic_tension.py +234 -0
- reasoning_forge/specialization_tracker.py +311 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.gguf filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
*.json filter=lfs diff=lfs merge=lfs -text
|
adapters/.gitkeep
ADDED
|
File without changes
|
adapters/consciousness-lora-f16.gguf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5c88d5e225e910402409cebaa9b330cba03bcd1330e8f1f069c9270353c269b5
|
| 3 |
+
size 27281088
|
adapters/convert_peft_to_gguf.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Convert PEFT LoRA safetensors to llama.cpp GGUF LoRA format.
|
| 3 |
+
|
| 4 |
+
Lightweight converter — no torch/transformers dependency.
|
| 5 |
+
Only needs: safetensors, gguf, numpy, struct.
|
| 6 |
+
|
| 7 |
+
Matches the exact format produced by llama.cpp's convert_lora_to_gguf.py.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import json
|
| 11 |
+
import struct
|
| 12 |
+
import sys
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
import numpy as np
|
| 15 |
+
|
| 16 |
+
# gguf uses its own writer
|
| 17 |
+
from gguf import GGUFWriter, GGMLQuantizationType
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# PEFT tensor name -> GGUF tensor name mapping for LLama
|
| 21 |
+
# PEFT: base_model.model.model.layers.{i}.self_attn.{proj}.lora_{AB}.weight
|
| 22 |
+
# GGUF: blk.{i}.attn_{mapped_proj}.weight.lora_{ab}
|
| 23 |
+
PROJ_MAP = {
|
| 24 |
+
"q_proj": "attn_q",
|
| 25 |
+
"k_proj": "attn_k",
|
| 26 |
+
"v_proj": "attn_v",
|
| 27 |
+
"o_proj": "attn_output",
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def bf16_to_f16(data_bytes: bytes) -> np.ndarray:
|
| 32 |
+
"""Convert bfloat16 raw bytes to float16 numpy array.
|
| 33 |
+
|
| 34 |
+
bf16: sign(1) + exp(8) + mantissa(7)
|
| 35 |
+
f16: sign(1) + exp(5) + mantissa(10)
|
| 36 |
+
|
| 37 |
+
We go bf16 -> f32 -> f16 to avoid precision edge cases.
|
| 38 |
+
"""
|
| 39 |
+
# Read as uint16 (same byte layout as bf16)
|
| 40 |
+
bf16 = np.frombuffer(data_bytes, dtype=np.uint16)
|
| 41 |
+
# Convert bf16 to f32: shift left 16 bits
|
| 42 |
+
f32_bytes = np.zeros(len(bf16), dtype=np.uint32)
|
| 43 |
+
f32_bytes[:] = bf16.astype(np.uint32) << 16
|
| 44 |
+
f32 = f32_bytes.view(np.float32)
|
| 45 |
+
# Convert f32 to f16
|
| 46 |
+
return f32.astype(np.float16)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def read_safetensors(path: Path) -> dict:
|
| 50 |
+
"""Read safetensors file, handling bf16 manually."""
|
| 51 |
+
with open(path, "rb") as f:
|
| 52 |
+
# Header: 8-byte little-endian uint64 = header size
|
| 53 |
+
header_size = struct.unpack("<Q", f.read(8))[0]
|
| 54 |
+
header_json = f.read(header_size)
|
| 55 |
+
header = json.loads(header_json)
|
| 56 |
+
|
| 57 |
+
data_start = 8 + header_size
|
| 58 |
+
tensors = {}
|
| 59 |
+
|
| 60 |
+
for name, info in header.items():
|
| 61 |
+
if name == "__metadata__":
|
| 62 |
+
continue
|
| 63 |
+
dtype = info["dtype"]
|
| 64 |
+
shape = info["shape"]
|
| 65 |
+
offsets = info["data_offsets"]
|
| 66 |
+
start, end = offsets
|
| 67 |
+
|
| 68 |
+
f.seek(data_start + start)
|
| 69 |
+
raw = f.read(end - start)
|
| 70 |
+
|
| 71 |
+
if dtype == "BF16":
|
| 72 |
+
arr = bf16_to_f16(raw).reshape(shape)
|
| 73 |
+
elif dtype == "F16":
|
| 74 |
+
arr = np.frombuffer(raw, dtype=np.float16).reshape(shape)
|
| 75 |
+
elif dtype == "F32":
|
| 76 |
+
arr = np.frombuffer(raw, dtype=np.float32).reshape(shape)
|
| 77 |
+
arr = arr.astype(np.float16)
|
| 78 |
+
else:
|
| 79 |
+
raise ValueError(f"Unsupported dtype: {dtype}")
|
| 80 |
+
|
| 81 |
+
tensors[name] = arr
|
| 82 |
+
|
| 83 |
+
return tensors
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def peft_name_to_gguf(peft_name: str) -> str | None:
|
| 87 |
+
"""Map PEFT tensor name to GGUF tensor name.
|
| 88 |
+
|
| 89 |
+
Input: base_model.model.model.layers.0.self_attn.q_proj.lora_A.weight
|
| 90 |
+
Output: blk.0.attn_q.weight.lora_a
|
| 91 |
+
"""
|
| 92 |
+
parts = peft_name.split(".")
|
| 93 |
+
# Expected: base_model.model.model.layers.{i}.self_attn.{proj}.lora_{AB}.weight
|
| 94 |
+
try:
|
| 95 |
+
layer_idx = parts[4] # layer number
|
| 96 |
+
proj = parts[6] # q_proj, k_proj, etc.
|
| 97 |
+
lora_part = parts[7] # lora_A or lora_B
|
| 98 |
+
except IndexError:
|
| 99 |
+
return None
|
| 100 |
+
|
| 101 |
+
gguf_proj = PROJ_MAP.get(proj)
|
| 102 |
+
if gguf_proj is None:
|
| 103 |
+
return None
|
| 104 |
+
|
| 105 |
+
ab = lora_part.lower() # lora_a or lora_b
|
| 106 |
+
return f"blk.{layer_idx}.{gguf_proj}.weight.{ab}"
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def convert(adapter_dir: Path, output_path: Path, adapter_name: str):
|
| 110 |
+
"""Convert a PEFT LoRA adapter to GGUF format."""
|
| 111 |
+
config_path = adapter_dir / "adapter_config.json"
|
| 112 |
+
safetensors_path = adapter_dir / "adapter_model.safetensors"
|
| 113 |
+
|
| 114 |
+
if not config_path.exists():
|
| 115 |
+
raise FileNotFoundError(f"No adapter_config.json in {adapter_dir}")
|
| 116 |
+
if not safetensors_path.exists():
|
| 117 |
+
raise FileNotFoundError(f"No adapter_model.safetensors in {adapter_dir}")
|
| 118 |
+
|
| 119 |
+
# Read config
|
| 120 |
+
with open(config_path) as f:
|
| 121 |
+
config = json.load(f)
|
| 122 |
+
|
| 123 |
+
lora_alpha = config.get("lora_alpha", 32)
|
| 124 |
+
lora_rank = config.get("r", 16)
|
| 125 |
+
print(f" Config: rank={lora_rank}, alpha={lora_alpha}")
|
| 126 |
+
|
| 127 |
+
# Read tensors
|
| 128 |
+
print(f" Reading safetensors...")
|
| 129 |
+
tensors = read_safetensors(safetensors_path)
|
| 130 |
+
print(f" Loaded {len(tensors)} tensors")
|
| 131 |
+
|
| 132 |
+
# Create GGUF writer
|
| 133 |
+
writer = GGUFWriter(str(output_path), arch="llama")
|
| 134 |
+
|
| 135 |
+
# Write metadata (matching the newton GGUF format exactly)
|
| 136 |
+
writer.add_string("general.type", "adapter")
|
| 137 |
+
writer.add_string("adapter.type", "lora")
|
| 138 |
+
writer.add_string("general.name", adapter_name)
|
| 139 |
+
writer.add_uint32("general.base_model.count", 1)
|
| 140 |
+
writer.add_string("general.base_model.0.name", "Llama 3.1 8B Instruct")
|
| 141 |
+
writer.add_string("general.base_model.0.organization", "Meta Llama")
|
| 142 |
+
writer.add_string("general.base_model.0.repo_url",
|
| 143 |
+
"https://huggingface.co/meta-llama/Llama-3.1-8B-Instruct")
|
| 144 |
+
writer.add_array("general.tags", [
|
| 145 |
+
"base_model:adapter:meta-llama/Llama-3.1-8B-Instruct",
|
| 146 |
+
"lora", "sft", "transformers", "trl", "text-generation",
|
| 147 |
+
])
|
| 148 |
+
writer.add_float32("adapter.lora.alpha", float(lora_alpha))
|
| 149 |
+
writer.add_uint32("general.quantization_version", 2)
|
| 150 |
+
|
| 151 |
+
# Convert and add tensors
|
| 152 |
+
converted = 0
|
| 153 |
+
for peft_name, data in sorted(tensors.items()):
|
| 154 |
+
gguf_name = peft_name_to_gguf(peft_name)
|
| 155 |
+
if gguf_name is None:
|
| 156 |
+
print(f" SKIP: {peft_name}")
|
| 157 |
+
continue
|
| 158 |
+
|
| 159 |
+
# GGUF LoRA expects F16 (type=1)
|
| 160 |
+
writer.add_tensor(gguf_name, data, raw_dtype=GGMLQuantizationType.F16)
|
| 161 |
+
converted += 1
|
| 162 |
+
|
| 163 |
+
print(f" Converted {converted} tensors")
|
| 164 |
+
|
| 165 |
+
# Write file
|
| 166 |
+
writer.write_header_to_file()
|
| 167 |
+
writer.write_kv_data_to_file()
|
| 168 |
+
writer.write_tensors_to_file()
|
| 169 |
+
writer.close()
|
| 170 |
+
|
| 171 |
+
size_mb = output_path.stat().st_size / 1024 / 1024
|
| 172 |
+
print(f" Output: {output_path} ({size_mb:.1f} MB)")
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def main():
|
| 176 |
+
adapters_dir = Path("J:/codette-training-lab/adapters")
|
| 177 |
+
hf_dir = adapters_dir / "hf_download"
|
| 178 |
+
|
| 179 |
+
# Convert all adapters that have safetensors but no GGUF yet
|
| 180 |
+
to_convert = []
|
| 181 |
+
for name in ["empathy", "philosophy", "quantum",
|
| 182 |
+
"consciousness", "multi_perspective", "systems_architecture"]:
|
| 183 |
+
src = hf_dir / name
|
| 184 |
+
dst = adapters_dir / f"{name}-lora-f16.gguf"
|
| 185 |
+
if src.exists() and (src / "adapter_model.safetensors").exists():
|
| 186 |
+
if dst.exists():
|
| 187 |
+
print(f"SKIP {name}: GGUF already exists")
|
| 188 |
+
else:
|
| 189 |
+
to_convert.append((name, src, dst))
|
| 190 |
+
else:
|
| 191 |
+
print(f"SKIP {name}: no safetensors found")
|
| 192 |
+
|
| 193 |
+
if not to_convert:
|
| 194 |
+
print("Nothing to convert!")
|
| 195 |
+
return
|
| 196 |
+
|
| 197 |
+
for name, src, dst in to_convert:
|
| 198 |
+
print(f"\nConverting {name}...")
|
| 199 |
+
try:
|
| 200 |
+
convert(src, dst, name)
|
| 201 |
+
print(f"OK: {name}")
|
| 202 |
+
except Exception as e:
|
| 203 |
+
print(f"FAIL: {name}: {e}")
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
if __name__ == "__main__":
|
| 207 |
+
main()
|
adapters/davinci-lora-f16.gguf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:008fa6af197f27c0635e0220766af8a67dbb0d76c51a00f4f6c9a6b0a8c06bb5
|
| 3 |
+
size 27281088
|
adapters/empathy-lora-f16.gguf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:37a7c5f74e9985ca0408fccbcc2640cea80a8c7694c3104ce4d059dade14855e
|
| 3 |
+
size 27281088
|
adapters/multi_perspective-lora-f16.gguf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f1f9e62a8250936a65ca5641f8994564fb7d15db4890c03f81462c06f178e04c
|
| 3 |
+
size 27281088
|
adapters/newton-lora-f16.gguf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9242685ec0cfbbc383237aeced0ee6f14676785a55930358350bccfab1db5a6f
|
| 3 |
+
size 27281088
|
adapters/philosophy-lora-f16.gguf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5157c68f663f0477164f5a7b95a5d89cea0966f8e41ebc49e58141abd96b329a
|
| 3 |
+
size 27281088
|
adapters/quantum-lora-f16.gguf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9c3e3bed6d81d7a72011b031150d81ec8911fa7820539db07f8a949f59a290ff
|
| 3 |
+
size 27281088
|
adapters/systems_architecture-lora-f16.gguf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a653d6c97a5c994d39aafa550a22ee6a23ea1b7f054ad81a595822969cd7f857
|
| 3 |
+
size 27281088
|
reasoning_forge/framework_definitions.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Phase 6: RC+xi Framework Mathematical Definitions
|
| 3 |
+
|
| 4 |
+
Formalizes three core concepts as first-class mathematical objects:
|
| 5 |
+
|
| 6 |
+
ψ (Psi/State): Cognitive state vector in 5D manifold
|
| 7 |
+
ψ = (ψ_psi, ψ_tau, ψ_chi, ψ_phi, ψ_lambda)
|
| 8 |
+
- ψ_psi ∈ [0, 1] : Concept magnitude (epistemic weight)
|
| 9 |
+
- ψ_tau ∈ [0, 1] : Temporal progression (causality)
|
| 10 |
+
- ψ_chi ∈ [-1, 2] : Processing velocity (agility)
|
| 11 |
+
- ψ_phi ∈ [-1, 1] : Emotional valence (ethical charge)
|
| 12 |
+
- ψ_lambda ∈ [0, 1] : Semantic diversity (concept breadth)
|
| 13 |
+
|
| 14 |
+
ξ (Xi/Tension): Epistemic tension between states
|
| 15 |
+
ξ_structural(ψ_a, ψ_b) = sqrt(sum((ψ_a_i - ψ_b_i)^2 for all 5 dimensions))
|
| 16 |
+
ξ_semantic(claim_a, claim_b) = 1.0 - cosine_similarity(embed(claim_a), embed(claim_b))
|
| 17 |
+
ξ_combined = w_struct * ξ_struct + w_semantic * ξ_semantic (weighted blend)
|
| 18 |
+
|
| 19 |
+
Γ (Gamma/Coherence): System health and integrity
|
| 20 |
+
Γ = (0.25 * perspective_diversity +
|
| 21 |
+
0.25 * tension_health +
|
| 22 |
+
0.25 * (1.0 - adapter_weight_variance) +
|
| 23 |
+
0.25 * resolution_rate)
|
| 24 |
+
Γ ∈ [0, 1]
|
| 25 |
+
- Γ < 0.4 : Collapse (monoculture/weight drift detected)
|
| 26 |
+
- 0.4 ≤ Γ ≤ 0.8: Healthy (productive tension)
|
| 27 |
+
- Γ > 0.8 : Groupthink (false consensus, enforce conflict)
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
from dataclasses import dataclass
|
| 31 |
+
from typing import List, Dict
|
| 32 |
+
import numpy as np
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@dataclass
|
| 36 |
+
class StateVector:
|
| 37 |
+
"""
|
| 38 |
+
ψ (Psi): Complete cognitive state in 5D manifold.
|
| 39 |
+
|
| 40 |
+
Used for:
|
| 41 |
+
- Representing query semantics in pre-flight prediction
|
| 42 |
+
- Encoding agent analyses for Spiderweb injection
|
| 43 |
+
- Measuring state-space distance between perspectives
|
| 44 |
+
"""
|
| 45 |
+
psi: float # [0, 1] concept magnitude / epistemic weight
|
| 46 |
+
tau: float # [0, 1] temporal progression / causality
|
| 47 |
+
chi: float # [-1, 2] processing velocity / agility
|
| 48 |
+
phi: float # [-1, 1] emotional valence / ethical charge
|
| 49 |
+
lam: float # [0, 1] semantic diversity / concept breadth
|
| 50 |
+
|
| 51 |
+
def to_array(self) -> np.ndarray:
|
| 52 |
+
"""Convert to numpy array for distance calculations."""
|
| 53 |
+
return np.array([self.psi, self.tau, self.chi, self.phi, self.lam], dtype=np.float32)
|
| 54 |
+
|
| 55 |
+
def to_dict(self) -> Dict:
|
| 56 |
+
"""Export as dictionary for JSON serialization."""
|
| 57 |
+
return {
|
| 58 |
+
"psi": round(self.psi, 3),
|
| 59 |
+
"tau": round(self.tau, 3),
|
| 60 |
+
"chi": round(self.chi, 3),
|
| 61 |
+
"phi": round(self.phi, 3),
|
| 62 |
+
"lam": round(self.lam, 3),
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
@staticmethod
|
| 66 |
+
def distance(state_a: "StateVector", state_b: "StateVector") -> float:
|
| 67 |
+
"""
|
| 68 |
+
Compute ξ_structural: Euclidean distance in 5D state space.
|
| 69 |
+
Range: [0, ~3.5] (theoretical max sqrt(4+4+9+4+1))
|
| 70 |
+
"""
|
| 71 |
+
arr_a = state_a.to_array()
|
| 72 |
+
arr_b = state_b.to_array()
|
| 73 |
+
return float(np.linalg.norm(arr_a - arr_b))
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@dataclass
|
| 77 |
+
class TensionDefinition:
|
| 78 |
+
"""
|
| 79 |
+
ξ (Xi): Complete specification of epistemic tension.
|
| 80 |
+
|
| 81 |
+
Blends structural (5D state distance) and semantic (embedding) components
|
| 82 |
+
for nuanced conflict detection.
|
| 83 |
+
"""
|
| 84 |
+
structural_xi: float # [0, ~3.5] 5D state distance
|
| 85 |
+
semantic_xi: float # [0, 1] embedding-based semantic distance
|
| 86 |
+
combined_xi: float # [0, ~2] weighted combination
|
| 87 |
+
opposition_type: str # "contradiction" | "emphasis" | "framework" | "paraphrase"
|
| 88 |
+
weight_structural: float # 0.4 default, tuneable
|
| 89 |
+
weight_semantic: float # 0.6 default, tuneable
|
| 90 |
+
|
| 91 |
+
def to_dict(self) -> Dict:
|
| 92 |
+
"""Export for analysis/benchmarking."""
|
| 93 |
+
return {
|
| 94 |
+
"structural_xi": round(self.structural_xi, 3),
|
| 95 |
+
"semantic_xi": round(self.semantic_xi, 3),
|
| 96 |
+
"combined_xi": round(self.combined_xi, 3),
|
| 97 |
+
"opposition_type": self.opposition_type,
|
| 98 |
+
"weight_structural": self.weight_structural,
|
| 99 |
+
"weight_semantic": self.weight_semantic,
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
@dataclass
|
| 104 |
+
class CoherenceMetrics:
|
| 105 |
+
"""
|
| 106 |
+
Γ (Gamma): Detailed characterization of system coherence/health.
|
| 107 |
+
|
| 108 |
+
Monitors four pillars; used by Phase 5 coherence_field to detect
|
| 109 |
+
collapse/groupthink and trigger interventions.
|
| 110 |
+
"""
|
| 111 |
+
perspective_diversity: float # [0, 1] uniqueness of agent perspectives
|
| 112 |
+
tension_health: float # [0, 1] productivity of epistemic tensions
|
| 113 |
+
adapter_weight_variance: float # [0, 1] distribution across adapters
|
| 114 |
+
resolution_rate: float # [0, 1] conflicts resolved per round
|
| 115 |
+
gamma_score: float # [0, 1] final coherence value
|
| 116 |
+
health_status: str # "collapsing" | "healthy" | "groupthinking"
|
| 117 |
+
|
| 118 |
+
@staticmethod
|
| 119 |
+
def compute_gamma(
|
| 120 |
+
perspective_diversity: float,
|
| 121 |
+
tension_health: float,
|
| 122 |
+
adapter_weight_variance: float,
|
| 123 |
+
resolution_rate: float,
|
| 124 |
+
) -> tuple:
|
| 125 |
+
"""
|
| 126 |
+
Compute Γ score from four pillars.
|
| 127 |
+
|
| 128 |
+
Returns: (gamma_score, health_status)
|
| 129 |
+
"""
|
| 130 |
+
gamma = (
|
| 131 |
+
0.25 * perspective_diversity
|
| 132 |
+
+ 0.25 * tension_health
|
| 133 |
+
+ 0.25 * (1.0 - adapter_weight_variance)
|
| 134 |
+
+ 0.25 * resolution_rate
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
# Determine health status
|
| 138 |
+
if gamma < 0.4:
|
| 139 |
+
status = "collapsing"
|
| 140 |
+
elif gamma > 0.8:
|
| 141 |
+
status = "groupthinking"
|
| 142 |
+
else:
|
| 143 |
+
status = "healthy"
|
| 144 |
+
|
| 145 |
+
return float(np.clip(gamma, 0.0, 1.0)), status
|
| 146 |
+
|
| 147 |
+
def to_dict(self) -> Dict:
|
| 148 |
+
"""Export for monitoring/logging."""
|
| 149 |
+
return {
|
| 150 |
+
"perspective_diversity": round(self.perspective_diversity, 3),
|
| 151 |
+
"tension_health": round(self.tension_health, 3),
|
| 152 |
+
"adapter_weight_variance": round(self.adapter_weight_variance, 3),
|
| 153 |
+
"resolution_rate": round(self.resolution_rate, 3),
|
| 154 |
+
"gamma_score": round(self.gamma_score, 3),
|
| 155 |
+
"health_status": self.health_status,
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
@dataclass
|
| 160 |
+
class ConflictPrediction:
|
| 161 |
+
"""
|
| 162 |
+
Output from pre-flight predictor.
|
| 163 |
+
|
| 164 |
+
Captures predicted conflicts, dimension-wise profiles, and router
|
| 165 |
+
recommendations before debate even begins.
|
| 166 |
+
"""
|
| 167 |
+
query_state: StateVector # Encoded query ψ
|
| 168 |
+
predicted_high_tension_pairs: List[Dict] # Agent pairs likely to conflict
|
| 169 |
+
conflict_profiles: Dict[str, List] # Grouped by dimension (phi, tau, chi, etc)
|
| 170 |
+
recommendations: Dict # {"boost": [...], "suppress": [...]}
|
| 171 |
+
preflight_confidence: float # [0, 1] how confident in prediction
|
| 172 |
+
|
| 173 |
+
def to_dict(self) -> Dict:
|
| 174 |
+
"""Export for metadata/analysis."""
|
| 175 |
+
return {
|
| 176 |
+
"query_state": self.query_state.to_dict(),
|
| 177 |
+
"predicted_pairs_count": len(self.predicted_high_tension_pairs),
|
| 178 |
+
"conflict_profiles": {k: len(v) for k, v in self.conflict_profiles.items()},
|
| 179 |
+
"recommendations": self.recommendations,
|
| 180 |
+
"preflight_confidence": round(self.preflight_confidence, 3),
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
@dataclass
|
| 185 |
+
class SpecializationScore:
|
| 186 |
+
"""
|
| 187 |
+
Measures adapter specialization within a domain.
|
| 188 |
+
|
| 189 |
+
specialization = domain_accuracy / usage_frequency
|
| 190 |
+
High score = expert in domain, not overused
|
| 191 |
+
Low score = either poor performance or overtaxed
|
| 192 |
+
"""
|
| 193 |
+
adapter: str # Adapter name
|
| 194 |
+
domain: str # "physics", "ethics", "consciousness", etc.
|
| 195 |
+
domain_accuracy: float # [0, 1] mean coherence in domain
|
| 196 |
+
usage_frequency: int # Times used in domain
|
| 197 |
+
specialization_score: float # domain_accuracy / max(usage, 1)
|
| 198 |
+
convergence_risk: bool # Semantic overlap with similar adapters > 0.85
|
| 199 |
+
recommendation: str # "maintain" | "boost" | "suppress" | "diversify"
|
| 200 |
+
|
| 201 |
+
def to_dict(self) -> Dict:
|
| 202 |
+
"""Export for adapter management."""
|
| 203 |
+
return {
|
| 204 |
+
"adapter": self.adapter,
|
| 205 |
+
"domain": self.domain,
|
| 206 |
+
"domain_accuracy": round(self.domain_accuracy, 3),
|
| 207 |
+
"usage_frequency": self.usage_frequency,
|
| 208 |
+
"specialization_score": round(self.specialization_score, 3),
|
| 209 |
+
"convergence_risk": self.convergence_risk,
|
| 210 |
+
"recommendation": self.recommendation,
|
| 211 |
+
}
|
reasoning_forge/preflight_predictor.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Phase 6: Pre-Flight Conflict Predictor
|
| 3 |
+
|
| 4 |
+
Uses Spiderweb to predict conflicts BEFORE debate starts.
|
| 5 |
+
|
| 6 |
+
Strategy:
|
| 7 |
+
1. Encode query into 5D state vector (ψ)
|
| 8 |
+
2. Inject into fresh spiderweb as virtual "truth" node
|
| 9 |
+
3. Propagate belief outward (3 hops max)
|
| 10 |
+
4. Measure resultant tensions per agent pair
|
| 11 |
+
5. Extract dimension-wise conflict profiles
|
| 12 |
+
6. Generate router recommendations (boost/suppress adapters)
|
| 13 |
+
|
| 14 |
+
This allows:
|
| 15 |
+
- Pre-selection of stabilizing adapters
|
| 16 |
+
- Reduction of wasted debate cycles on predictable conflicts
|
| 17 |
+
- Faster convergence via informed initial routing
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from typing import Dict, List, Tuple, Optional
|
| 21 |
+
import numpy as np
|
| 22 |
+
from dataclasses import dataclass
|
| 23 |
+
from reasoning_forge.framework_definitions import StateVector, ConflictPrediction
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@dataclass
|
| 27 |
+
class DimensionConflict:
|
| 28 |
+
"""Conflict localized to specific 5D dimension."""
|
| 29 |
+
dimension: str # "psi", "tau", "chi", "phi", "lam"
|
| 30 |
+
agent_a: str
|
| 31 |
+
agent_b: str
|
| 32 |
+
dimension_diff: float # How far apart in this dimension
|
| 33 |
+
severity: str # "low" | "medium" | "high"
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class PreFlightConflictPredictor:
|
| 37 |
+
"""
|
| 38 |
+
Predicts conflicts before debate using Spiderweb injection.
|
| 39 |
+
|
| 40 |
+
Assumes Spiderweb has:
|
| 41 |
+
- add_node(name, state=StateVector)
|
| 42 |
+
- connect(node_a, node_b)
|
| 43 |
+
- propagate_belief(origin, belief, max_hops) -> propagation_result
|
| 44 |
+
- nodes: Dict[name, NodeState]
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
def __init__(self, spiderweb, memory_weighting=None, semantic_engine=None):
|
| 48 |
+
"""
|
| 49 |
+
Initialize predictor with Spiderweb instance.
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
spiderweb: QuantumSpiderweb instance
|
| 53 |
+
memory_weighting: Optional MemoryWeighting for boost recommendations
|
| 54 |
+
semantic_engine: Optional SemanticTensionEngine for enhanced predictions
|
| 55 |
+
"""
|
| 56 |
+
self.spiderweb = spiderweb
|
| 57 |
+
self.memory_weighting = memory_weighting
|
| 58 |
+
self.semantic_engine = semantic_engine
|
| 59 |
+
self.prediction_history = []
|
| 60 |
+
|
| 61 |
+
def encode_query_to_state(self, query: str) -> StateVector:
|
| 62 |
+
"""
|
| 63 |
+
Convert query text to 5D state vector (ψ).
|
| 64 |
+
|
| 65 |
+
Heuristic encoding:
|
| 66 |
+
- ψ_psi: concept_magnitude (TF-IDF norm of key concepts)
|
| 67 |
+
- ψ_tau: temporal_progression (presence of causality/time markers)
|
| 68 |
+
- ψ_chi: processing_velocity (query complexity / baseline)
|
| 69 |
+
- ψ_phi: emotional_valence (sentiment + ethical keywords)
|
| 70 |
+
- ψ_lambda: semantic_diversity (unique_concepts / total)
|
| 71 |
+
|
| 72 |
+
Returns:
|
| 73 |
+
StateVector with 5D values
|
| 74 |
+
"""
|
| 75 |
+
query_lower = query.lower()
|
| 76 |
+
tokens = query_lower.split()
|
| 77 |
+
|
| 78 |
+
# ψ_psi: Concept magnitude from query length and key concept presence
|
| 79 |
+
key_concepts = ["what", "how", "why", "should", "could", "would", "is", "can"]
|
| 80 |
+
concept_count = sum(1 for t in tokens if t in key_concepts)
|
| 81 |
+
psi = min(1.0, (len(tokens) / 20.0) * 0.5 + (concept_count / 10.0) * 0.5)
|
| 82 |
+
|
| 83 |
+
# ψ_tau: Temporal progression markers
|
| 84 |
+
temporal_markers = ["past", "future", "before", "after", "then", "now", "when", "time", "history"]
|
| 85 |
+
tau = min(1.0, sum(1 for m in temporal_markers if m in query_lower) / 10.0)
|
| 86 |
+
|
| 87 |
+
# ψ_chi: Processing complexity
|
| 88 |
+
# Sentence-like structures (questions, nested clauses)
|
| 89 |
+
complexity_markers = ["that", "whether", "if", "and", "or", "but", "however"]
|
| 90 |
+
chi_complexity = sum(1 for m in complexity_markers if m in query_lower) / 5.0
|
| 91 |
+
# Normalize to [-1, 2]
|
| 92 |
+
chi = max(-1.0, min(2.0, (chi_complexity - 0.5) * 2.0))
|
| 93 |
+
|
| 94 |
+
# ψ_phi: Emotional/ethical valence
|
| 95 |
+
positive_words = ["good", "right", "better", "best", "love", "beautiful"]
|
| 96 |
+
negative_words = ["bad", "wrong", "worse", "hate", "ugly"]
|
| 97 |
+
ethical_words = ["should", "must", "moral", "ethics", "justice", "fair"]
|
| 98 |
+
|
| 99 |
+
pos_count = sum(1 for w in positive_words if w in query_lower)
|
| 100 |
+
neg_count = sum(1 for w in negative_words if w in query_lower)
|
| 101 |
+
eth_count = sum(1 for w in ethical_words if w in query_lower)
|
| 102 |
+
|
| 103 |
+
sentiment = (pos_count - neg_count) / max(pos_count + neg_count, 1)
|
| 104 |
+
ethics_density = eth_count / len(tokens) if tokens else 0
|
| 105 |
+
phi = np.tanh((sentiment + ethics_density * 0.5)) # Squash to [-1, 1]
|
| 106 |
+
|
| 107 |
+
# ψ_lambda: Semantic diversity
|
| 108 |
+
unique_tokens = len(set(tokens))
|
| 109 |
+
total_tokens = len(tokens)
|
| 110 |
+
lam = unique_tokens / max(total_tokens, 1)
|
| 111 |
+
|
| 112 |
+
query_state = StateVector(
|
| 113 |
+
psi=float(np.clip(psi, 0.0, 1.0)),
|
| 114 |
+
tau=float(np.clip(tau, 0.0, 1.0)),
|
| 115 |
+
chi=float(np.clip(chi, -1.0, 2.0)),
|
| 116 |
+
phi=float(np.clip(phi, -1.0, 1.0)),
|
| 117 |
+
lam=float(np.clip(lam, 0.0, 1.0)),
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
return query_state
|
| 121 |
+
|
| 122 |
+
def predict_conflicts(
|
| 123 |
+
self, query: str, agent_names: List[str], max_hops: int = 3
|
| 124 |
+
) -> ConflictPrediction:
|
| 125 |
+
"""
|
| 126 |
+
Predict conflicts using spiderweb belief propagation.
|
| 127 |
+
|
| 128 |
+
Args:
|
| 129 |
+
query: Query text
|
| 130 |
+
agent_names: List of agent/adapter names
|
| 131 |
+
max_hops: Maximum propagation distance
|
| 132 |
+
|
| 133 |
+
Returns:
|
| 134 |
+
ConflictPrediction with predicted pairs, profiles, recommendations
|
| 135 |
+
"""
|
| 136 |
+
query_state = self.encode_query_to_state(query)
|
| 137 |
+
|
| 138 |
+
# Build fresh spiderweb from agents
|
| 139 |
+
try:
|
| 140 |
+
self.spiderweb.build_from_agents(agent_names)
|
| 141 |
+
except Exception as e:
|
| 142 |
+
print(f"Warning: Could not build spiderweb: {e}")
|
| 143 |
+
return self._empty_prediction(query_state)
|
| 144 |
+
|
| 145 |
+
# Add query as virtual node
|
| 146 |
+
try:
|
| 147 |
+
self.spiderweb.add_node("_QUERY", state=query_state)
|
| 148 |
+
if len(agent_names) > 0:
|
| 149 |
+
self.spiderweb.connect("_QUERY", agent_names[0])
|
| 150 |
+
except Exception as e:
|
| 151 |
+
print(f"Warning: Could not add query node: {e}")
|
| 152 |
+
return self._empty_prediction(query_state)
|
| 153 |
+
|
| 154 |
+
# Propagate belief
|
| 155 |
+
try:
|
| 156 |
+
propagation = self.spiderweb.propagate_belief(
|
| 157 |
+
origin="_QUERY", belief=query_state, max_hops=max_hops
|
| 158 |
+
)
|
| 159 |
+
except Exception as e:
|
| 160 |
+
print(f"Warning: Propagation failed: {e}")
|
| 161 |
+
return self._empty_prediction(query_state)
|
| 162 |
+
|
| 163 |
+
# Analyze tensions and extract profiles
|
| 164 |
+
high_tension_pairs = self._analyze_tensions(propagation, agent_names)
|
| 165 |
+
conflict_profiles = self._extract_conflict_profiles(high_tension_pairs)
|
| 166 |
+
|
| 167 |
+
# Generate recommendations
|
| 168 |
+
recommendations = self._generate_recommendations(conflict_profiles)
|
| 169 |
+
|
| 170 |
+
# Compute confidence in predictions
|
| 171 |
+
preflight_confidence = self._compute_prediction_confidence(high_tension_pairs, agent_names)
|
| 172 |
+
|
| 173 |
+
prediction = ConflictPrediction(
|
| 174 |
+
query_state=query_state,
|
| 175 |
+
predicted_high_tension_pairs=high_tension_pairs,
|
| 176 |
+
conflict_profiles=conflict_profiles,
|
| 177 |
+
recommendations=recommendations,
|
| 178 |
+
preflight_confidence=preflight_confidence,
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
self.prediction_history.append(prediction)
|
| 182 |
+
|
| 183 |
+
return prediction
|
| 184 |
+
|
| 185 |
+
def _analyze_tensions(self, propagation: Dict, agent_names: List[str]) -> List[Dict]:
|
| 186 |
+
"""
|
| 187 |
+
Extract high-tension agent pairs from propagation results.
|
| 188 |
+
|
| 189 |
+
Returns:
|
| 190 |
+
List of {agent_a, agent_b, spiderweb_tension, dimension_breakdown}
|
| 191 |
+
"""
|
| 192 |
+
high_tension_pairs = []
|
| 193 |
+
|
| 194 |
+
# Look for nodes in spiderweb
|
| 195 |
+
if not hasattr(self.spiderweb, "nodes"):
|
| 196 |
+
return high_tension_pairs
|
| 197 |
+
|
| 198 |
+
nodes = self.spiderweb.nodes
|
| 199 |
+
valid_agents = [a for a in agent_names if a in nodes]
|
| 200 |
+
|
| 201 |
+
# Measure pairwise tensions
|
| 202 |
+
for i, agent_a in enumerate(valid_agents):
|
| 203 |
+
for agent_b in valid_agents[i + 1 :]:
|
| 204 |
+
try:
|
| 205 |
+
state_a = nodes[agent_a].state if hasattr(nodes[agent_a], "state") else None
|
| 206 |
+
state_b = nodes[agent_b].state if hasattr(nodes[agent_b], "state") else None
|
| 207 |
+
|
| 208 |
+
if state_a and state_b:
|
| 209 |
+
# Compute 5D distance
|
| 210 |
+
xi_structural = StateVector.distance(state_a, state_b)
|
| 211 |
+
|
| 212 |
+
if xi_structural > 1.0: # Only flag significant tensions
|
| 213 |
+
# Dimension-wise breakdown
|
| 214 |
+
arr_a = state_a.to_array()
|
| 215 |
+
arr_b = state_b.to_array()
|
| 216 |
+
diffs = arr_b - arr_a
|
| 217 |
+
|
| 218 |
+
dimension_names = ["psi", "tau", "chi", "phi", "lam"]
|
| 219 |
+
|
| 220 |
+
high_tension_pairs.append({
|
| 221 |
+
"agent_a": agent_a,
|
| 222 |
+
"agent_b": agent_b,
|
| 223 |
+
"spiderweb_tension": round(xi_structural, 3),
|
| 224 |
+
"dimension_breakdown": {
|
| 225 |
+
dim: round(abs(diff), 3) for dim, diff in zip(dimension_names, diffs)
|
| 226 |
+
},
|
| 227 |
+
})
|
| 228 |
+
except Exception:
|
| 229 |
+
pass
|
| 230 |
+
|
| 231 |
+
# Sort by tension (strongest first)
|
| 232 |
+
high_tension_pairs.sort(key=lambda p: p["spiderweb_tension"], reverse=True)
|
| 233 |
+
|
| 234 |
+
return high_tension_pairs[:10] # Top 10 pairs
|
| 235 |
+
|
| 236 |
+
def _extract_conflict_profiles(self, high_tension_pairs: List[Dict]) -> Dict[str, List]:
|
| 237 |
+
"""
|
| 238 |
+
Group conflicts by dimension to identify patterns.
|
| 239 |
+
|
| 240 |
+
Returns:
|
| 241 |
+
{
|
| 242 |
+
"psi_conflicts": [{pair, diff}],
|
| 243 |
+
"tau_conflicts": [...],
|
| 244 |
+
...
|
| 245 |
+
"lam_conflicts": [...]
|
| 246 |
+
}
|
| 247 |
+
"""
|
| 248 |
+
profiles = {
|
| 249 |
+
"psi_conflicts": [],
|
| 250 |
+
"tau_conflicts": [],
|
| 251 |
+
"chi_conflicts": [],
|
| 252 |
+
"phi_conflicts": [],
|
| 253 |
+
"lam_conflicts": [],
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
threshold = 0.4 # Flag if dimension diff > threshold
|
| 257 |
+
|
| 258 |
+
for pair in high_tension_pairs:
|
| 259 |
+
breakdown = pair["dimension_breakdown"]
|
| 260 |
+
|
| 261 |
+
if breakdown.get("psi", 0) > threshold:
|
| 262 |
+
profiles["psi_conflicts"].append(pair)
|
| 263 |
+
if breakdown.get("tau", 0) > threshold:
|
| 264 |
+
profiles["tau_conflicts"].append(pair)
|
| 265 |
+
if breakdown.get("chi", 0) > threshold:
|
| 266 |
+
profiles["chi_conflicts"].append(pair)
|
| 267 |
+
if breakdown.get("phi", 0) > threshold:
|
| 268 |
+
profiles["phi_conflicts"].append(pair)
|
| 269 |
+
if breakdown.get("lam", 0) > threshold:
|
| 270 |
+
profiles["lam_conflicts"].append(pair)
|
| 271 |
+
|
| 272 |
+
return profiles
|
| 273 |
+
|
| 274 |
+
def _generate_recommendations(self, profiles: Dict[str, List]) -> Dict:
|
| 275 |
+
"""
|
| 276 |
+
Generate adapter boost/suppress recommendations based on conflict profiles.
|
| 277 |
+
|
| 278 |
+
Logic:
|
| 279 |
+
- phi_conflicts (ethical divergence) → boost Empathy, Ethics
|
| 280 |
+
- tau_conflicts (temporal framing) → boost Philosophy
|
| 281 |
+
- chi_conflicts (complexity mismatch) → boost multi_perspective
|
| 282 |
+
- lam_conflicts (semantic diversity) → boost consciousness
|
| 283 |
+
- psi_conflicts (concept magnitude) → boost newton (analytical)
|
| 284 |
+
"""
|
| 285 |
+
recommendations = {
|
| 286 |
+
"boost": [],
|
| 287 |
+
"suppress": [],
|
| 288 |
+
"reason": None,
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
# Count conflicts per dimension
|
| 292 |
+
counts = {k: len(v) for k, v in profiles.items()}
|
| 293 |
+
max_conflicts = max(counts.values()) if counts else 0
|
| 294 |
+
|
| 295 |
+
if counts.get("phi_conflicts", 0) >= 2:
|
| 296 |
+
recommendations["boost"] = ["empathy", "philosophy"]
|
| 297 |
+
recommendations["reason"] = "emotional_and_ethical_divergence"
|
| 298 |
+
elif counts.get("tau_conflicts", 0) >= 2:
|
| 299 |
+
recommendations["boost"] = ["philosophy"]
|
| 300 |
+
recommendations["reason"] = "temporal_framing_divergence"
|
| 301 |
+
elif counts.get("chi_conflicts", 0) >= 2:
|
| 302 |
+
recommendations["boost"] = ["multi_perspective"]
|
| 303 |
+
recommendations["reason"] = "complexity_divergence"
|
| 304 |
+
elif counts.get("lam_conflicts", 0) >= 2:
|
| 305 |
+
recommendations["boost"] = ["consciousness"]
|
| 306 |
+
recommendations["reason"] = "semantic_diversity_divergence"
|
| 307 |
+
elif counts.get("psi_conflicts", 0) >= 2:
|
| 308 |
+
recommendations["boost"] = ["newton"]
|
| 309 |
+
recommendations["reason"] = "conceptual_magnitude_divergence"
|
| 310 |
+
|
| 311 |
+
return recommendations
|
| 312 |
+
|
| 313 |
+
def _compute_prediction_confidence(self, pairs: List[Dict], agent_names: List[str]) -> float:
|
| 314 |
+
"""
|
| 315 |
+
Estimate confidence in pre-flight predictions.
|
| 316 |
+
|
| 317 |
+
Higher if:
|
| 318 |
+
- More agents involved
|
| 319 |
+
- Consistent patterns across pairs
|
| 320 |
+
- Previous predictions matched actual conflicts
|
| 321 |
+
"""
|
| 322 |
+
if not pairs or not agent_names:
|
| 323 |
+
return 0.3
|
| 324 |
+
|
| 325 |
+
# Base confidence from number of predicted pairs
|
| 326 |
+
confidence = min(1.0, len(pairs) / len(agent_names))
|
| 327 |
+
|
| 328 |
+
# Boost if clear patterns (multiple conflicts in same dimension)
|
| 329 |
+
return float(np.clip(confidence, 0.3, 0.95))
|
| 330 |
+
|
| 331 |
+
def _empty_prediction(self, query_state: StateVector) -> ConflictPrediction:
|
| 332 |
+
"""Return safe empty prediction if propagation failed."""
|
| 333 |
+
return ConflictPrediction(
|
| 334 |
+
query_state=query_state,
|
| 335 |
+
predicted_high_tension_pairs=[],
|
| 336 |
+
conflict_profiles={},
|
| 337 |
+
recommendations={"boost": [], "suppress": [], "reason": "no_prediction"},
|
| 338 |
+
preflight_confidence=0.0,
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
def get_prediction_history(self, limit: int = 10) -> List[Dict]:
|
| 342 |
+
"""Get recent predictions for analysis."""
|
| 343 |
+
recent = self.prediction_history[-limit:]
|
| 344 |
+
return [p.to_dict() for p in recent]
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
__all__ = ["PreFlightConflictPredictor"]
|
reasoning_forge/query_classifier.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Query Complexity Classifier
|
| 2 |
+
|
| 3 |
+
Determines whether a query needs full debate or can be answered directly.
|
| 4 |
+
|
| 5 |
+
This prevents over-activation: simple factual questions get direct answers,
|
| 6 |
+
while complex/ambiguous questions trigger full multi-agent reasoning.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import re
|
| 10 |
+
from enum import Enum
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class QueryComplexity(Enum):
|
| 14 |
+
"""Query complexity levels"""
|
| 15 |
+
SIMPLE = "simple" # Direct factual answer, no debate needed
|
| 16 |
+
MEDIUM = "medium" # Limited debate (2-3 agents)
|
| 17 |
+
COMPLEX = "complex" # Full debate with all relevant agents
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class QueryClassifier:
|
| 21 |
+
"""Classify query complexity to determine reasoning depth."""
|
| 22 |
+
|
| 23 |
+
# Factual keywords (SIMPLE queries)
|
| 24 |
+
FACTUAL_PATTERNS = [
|
| 25 |
+
r"what is .*\?", # "What is the speed of light?"
|
| 26 |
+
r"define ", # "Define entropy"
|
| 27 |
+
r"what (year|date|time) ", # "What year did..."
|
| 28 |
+
r"how fast is", # "How fast is..."
|
| 29 |
+
r"how high is",
|
| 30 |
+
r"how long is",
|
| 31 |
+
r"what (color|size|shape)",
|
| 32 |
+
r"who is .*\?$", # "Who is Einstein?"
|
| 33 |
+
r"where (is|are)", # "Where is the capital?"
|
| 34 |
+
r"list of ", # "List of elements"
|
| 35 |
+
r"formula for", # "Formula for..."
|
| 36 |
+
r"calculate ", # "Calculate..."
|
| 37 |
+
]
|
| 38 |
+
|
| 39 |
+
# Ambiguous keywords (COMPLEX queries)
|
| 40 |
+
AMBIGUOUS_PATTERNS = [
|
| 41 |
+
r"could|might|may|possibly", # Uncertainty
|
| 42 |
+
r"what does .* mean", # Interpretation
|
| 43 |
+
r"why", # Explanation (often multi-faceted)
|
| 44 |
+
r"how (do|does|should)", # Process/methodology
|
| 45 |
+
r"discuss",
|
| 46 |
+
r"compare",
|
| 47 |
+
r"contrast",
|
| 48 |
+
r"relationship between",
|
| 49 |
+
r"difference between",
|
| 50 |
+
]
|
| 51 |
+
|
| 52 |
+
# Ethics/Philosophy keywords (COMPLEX queries)
|
| 53 |
+
ETHICS_PATTERNS = [
|
| 54 |
+
r"should (we |i )",
|
| 55 |
+
r"is it (right|wrong|ethical|moral)",
|
| 56 |
+
r"is it (good|bad|fair)",
|
| 57 |
+
r"ought",
|
| 58 |
+
r"morally?",
|
| 59 |
+
r"ethics?",
|
| 60 |
+
r"value of",
|
| 61 |
+
r"meaning of",
|
| 62 |
+
r"purpose of",
|
| 63 |
+
r"implications of",
|
| 64 |
+
]
|
| 65 |
+
|
| 66 |
+
# Multi-domain keywords (COMPLEX queries)
|
| 67 |
+
MULTIDOMAIN_PATTERNS = [
|
| 68 |
+
r"connect .* to",
|
| 69 |
+
r"relate .* to",
|
| 70 |
+
r"how does .* affect",
|
| 71 |
+
r"impact (of|on)",
|
| 72 |
+
r"relationship .*between",
|
| 73 |
+
r"interaction .*between",
|
| 74 |
+
]
|
| 75 |
+
|
| 76 |
+
# Subjective/opinion keywords (COMPLEX queries)
|
| 77 |
+
SUBJECTIVE_PATTERNS = [
|
| 78 |
+
r"think",
|
| 79 |
+
r"opinion",
|
| 80 |
+
r"perspective",
|
| 81 |
+
r"view(point)?",
|
| 82 |
+
r"argue(ment)?",
|
| 83 |
+
r"debate",
|
| 84 |
+
r"controversy",
|
| 85 |
+
r"controversial",
|
| 86 |
+
]
|
| 87 |
+
|
| 88 |
+
def classify(self, query: str) -> QueryComplexity:
|
| 89 |
+
"""Classify query complexity.
|
| 90 |
+
|
| 91 |
+
Args:
|
| 92 |
+
query: The user query
|
| 93 |
+
|
| 94 |
+
Returns:
|
| 95 |
+
QueryComplexity level (SIMPLE, MEDIUM, or COMPLEX)
|
| 96 |
+
"""
|
| 97 |
+
query_lower = query.lower().strip()
|
| 98 |
+
|
| 99 |
+
# SIMPLE: Pure factual queries
|
| 100 |
+
if self._is_factual(query_lower):
|
| 101 |
+
# But check if it has complexity markers too
|
| 102 |
+
if self._has_ambiguity(query_lower) or self._has_ethics(query_lower):
|
| 103 |
+
return QueryComplexity.COMPLEX
|
| 104 |
+
return QueryComplexity.SIMPLE
|
| 105 |
+
|
| 106 |
+
# COMPLEX: Ethics, philosophy, interpretation, multi-domain
|
| 107 |
+
if self._has_ethics(query_lower):
|
| 108 |
+
return QueryComplexity.COMPLEX
|
| 109 |
+
if self._has_ambiguity(query_lower):
|
| 110 |
+
return QueryComplexity.COMPLEX
|
| 111 |
+
if self._has_multidomain(query_lower):
|
| 112 |
+
return QueryComplexity.COMPLEX
|
| 113 |
+
if self._has_subjective(query_lower):
|
| 114 |
+
return QueryComplexity.COMPLEX
|
| 115 |
+
|
| 116 |
+
# MEDIUM: Everything else
|
| 117 |
+
return QueryComplexity.MEDIUM
|
| 118 |
+
|
| 119 |
+
def _is_factual(self, query: str) -> bool:
|
| 120 |
+
"""Check if query is direct factual question."""
|
| 121 |
+
return any(re.search(pattern, query) for pattern in self.FACTUAL_PATTERNS)
|
| 122 |
+
|
| 123 |
+
def _has_ambiguity(self, query: str) -> bool:
|
| 124 |
+
"""Check if query has ambiguity markers."""
|
| 125 |
+
return any(re.search(pattern, query) for pattern in self.AMBIGUOUS_PATTERNS)
|
| 126 |
+
|
| 127 |
+
def _has_ethics(self, query: str) -> bool:
|
| 128 |
+
"""Check if query involves ethics/philosophy."""
|
| 129 |
+
return any(re.search(pattern, query) for pattern in self.ETHICS_PATTERNS)
|
| 130 |
+
|
| 131 |
+
def _has_multidomain(self, query: str) -> bool:
|
| 132 |
+
"""Check if query spans multiple domains."""
|
| 133 |
+
return any(re.search(pattern, query) for pattern in self.MULTIDOMAIN_PATTERNS)
|
| 134 |
+
|
| 135 |
+
def _has_subjective(self, query: str) -> bool:
|
| 136 |
+
"""Check if query invites subjective reasoning."""
|
| 137 |
+
return any(re.search(pattern, query) for pattern in self.SUBJECTIVE_PATTERNS)
|
| 138 |
+
|
| 139 |
+
def select_agents(
|
| 140 |
+
self, complexity: QueryComplexity, domain: str
|
| 141 |
+
) -> dict[str, float]:
|
| 142 |
+
"""Select agents and their weights based on complexity and domain.
|
| 143 |
+
|
| 144 |
+
Args:
|
| 145 |
+
complexity: Query complexity level
|
| 146 |
+
domain: Detected query domain
|
| 147 |
+
|
| 148 |
+
Returns:
|
| 149 |
+
Dict mapping agent names to activation weights (0-1)
|
| 150 |
+
"""
|
| 151 |
+
# All available agents with their domains
|
| 152 |
+
all_agents = {
|
| 153 |
+
"Newton": ["physics", "mathematics", "systems"],
|
| 154 |
+
"Quantum": ["physics", "uncertainty", "systems"],
|
| 155 |
+
"Philosophy": ["philosophy", "meaning", "consciousness"],
|
| 156 |
+
"DaVinci": ["creativity", "systems", "innovation"],
|
| 157 |
+
"Empathy": ["ethics", "consciousness", "meaning"],
|
| 158 |
+
"Ethics": ["ethics", "consciousness", "meaning"],
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
domain_agents = all_agents
|
| 162 |
+
|
| 163 |
+
if complexity == QueryComplexity.SIMPLE:
|
| 164 |
+
# Simple queries: just the primary agent for the domain
|
| 165 |
+
# Activate only 1 agent at full strength
|
| 166 |
+
primary = self._get_primary_agent(domain)
|
| 167 |
+
return {primary: 1.0}
|
| 168 |
+
|
| 169 |
+
elif complexity == QueryComplexity.MEDIUM:
|
| 170 |
+
# Medium queries: primary + 1-2 secondary agents
|
| 171 |
+
# Soft gating with weighted influence
|
| 172 |
+
primary = self._get_primary_agent(domain)
|
| 173 |
+
secondaries = self._get_secondary_agents(domain, count=1)
|
| 174 |
+
|
| 175 |
+
weights = {primary: 1.0}
|
| 176 |
+
for secondary in secondaries:
|
| 177 |
+
weights[secondary] = 0.6
|
| 178 |
+
|
| 179 |
+
return weights
|
| 180 |
+
|
| 181 |
+
else: # COMPLEX
|
| 182 |
+
# Complex queries: all relevant agents for domain + cross-domain
|
| 183 |
+
# Full soft gating
|
| 184 |
+
primary = self._get_primary_agent(domain)
|
| 185 |
+
secondaries = self._get_secondary_agents(domain, count=2)
|
| 186 |
+
cross_domain = self._get_cross_domain_agents(domain, count=1)
|
| 187 |
+
|
| 188 |
+
weights = {primary: 1.0}
|
| 189 |
+
for secondary in secondaries:
|
| 190 |
+
weights[secondary] = 0.7
|
| 191 |
+
for cross in cross_domain:
|
| 192 |
+
weights[cross] = 0.4
|
| 193 |
+
|
| 194 |
+
return weights
|
| 195 |
+
|
| 196 |
+
def _get_primary_agent(self, domain: str) -> str:
|
| 197 |
+
"""Get the primary agent for a domain."""
|
| 198 |
+
domain_map = {
|
| 199 |
+
"physics": "Newton",
|
| 200 |
+
"mathematics": "Newton",
|
| 201 |
+
"creativity": "DaVinci",
|
| 202 |
+
"ethics": "Ethics",
|
| 203 |
+
"philosophy": "Philosophy",
|
| 204 |
+
"meaning": "Philosophy",
|
| 205 |
+
"consciousness": "Empathy",
|
| 206 |
+
"uncertainty": "Quantum",
|
| 207 |
+
"systems": "Newton",
|
| 208 |
+
}
|
| 209 |
+
return domain_map.get(domain, "Newton")
|
| 210 |
+
|
| 211 |
+
def _get_secondary_agents(self, domain: str, count: int = 1) -> list[str]:
|
| 212 |
+
"""Get secondary agents for a domain."""
|
| 213 |
+
domain_map = {
|
| 214 |
+
"physics": ["Quantum", "DaVinci"],
|
| 215 |
+
"mathematics": ["Quantum", "Philosophy"],
|
| 216 |
+
"creativity": ["Quantum", "Empathy"],
|
| 217 |
+
"ethics": ["Philosophy", "Empathy"],
|
| 218 |
+
"philosophy": ["Empathy", "Ethics"],
|
| 219 |
+
"meaning": ["Quantum", "DaVinci"],
|
| 220 |
+
"consciousness": ["Philosophy", "Quantum"],
|
| 221 |
+
"uncertainty": ["Philosophy", "DaVinci"],
|
| 222 |
+
"systems": ["DaVinci", "Philosophy"],
|
| 223 |
+
}
|
| 224 |
+
candidates = domain_map.get(domain, ["Philosophy", "DaVinci"])
|
| 225 |
+
return candidates[:count]
|
| 226 |
+
|
| 227 |
+
def _get_cross_domain_agents(self, domain: str, count: int = 1) -> list[str]:
|
| 228 |
+
"""Get cross-domain agents (useful for all domains)."""
|
| 229 |
+
# Philosophy and Empathy are useful everywhere
|
| 230 |
+
candidates = ["Philosophy", "Empathy", "DaVinci"]
|
| 231 |
+
return candidates[:count]
|
reasoning_forge/semantic_tension.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Phase 6: Semantic Tension Engine
|
| 3 |
+
|
| 4 |
+
Computes ξ_semantic using Llama-3.1-8B embeddings instead of token heuristics.
|
| 5 |
+
Replaces discrete opposition_score (0.4/0.7/1.0) with continuous [0, 1] semantic distance.
|
| 6 |
+
|
| 7 |
+
Key innovation: Embedding-based tension captures *real disagreement*, not just
|
| 8 |
+
syntactic differences or confidence levels.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from typing import Dict, Tuple
|
| 12 |
+
import numpy as np
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class SemanticTensionEngine:
|
| 16 |
+
"""
|
| 17 |
+
Computes semantic tension (ξ_semantic) between claims using Llama embeddings.
|
| 18 |
+
|
| 19 |
+
Strategy:
|
| 20 |
+
1. Embed claims using Llama's final hidden layer
|
| 21 |
+
2. Normalize embeddings (L2)
|
| 22 |
+
3. Compute cosine similarity
|
| 23 |
+
4. Convert to tension: ξ = 1.0 - similarity
|
| 24 |
+
|
| 25 |
+
Benefits over heuristic opposition_score:
|
| 26 |
+
- Captures semantic meaning, not just tokens or contradiction keywords
|
| 27 |
+
- Continuous [0, 1] range reveals nuance (not discrete 0.4/0.7/1.0)
|
| 28 |
+
- Robust to paraphrasing (similar meaning = low tension)
|
| 29 |
+
- Detects orthogonal concepts (framework divergence)
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
def __init__(self, llama_model=None):
|
| 33 |
+
"""
|
| 34 |
+
Initialize with Llama model for embeddings.
|
| 35 |
+
|
| 36 |
+
Args:
|
| 37 |
+
llama_model: Llama-3.1-8B instance with .encode() method,
|
| 38 |
+
or None for testing (will use dummy embeddings)
|
| 39 |
+
"""
|
| 40 |
+
self.model = llama_model
|
| 41 |
+
self.embedding_cache = {} # {claim_text: embedding_vector}
|
| 42 |
+
self.embedding_dim = 4096 # Llama-3.1-8B hidden state dimension
|
| 43 |
+
|
| 44 |
+
def embed_claim(self, claim: str, use_cache: bool = True) -> np.ndarray:
|
| 45 |
+
"""
|
| 46 |
+
Get normalized embedding from Llama for a claim.
|
| 47 |
+
|
| 48 |
+
Args:
|
| 49 |
+
claim: Text claim to embed
|
| 50 |
+
use_cache: If True, reuse cached embeddings
|
| 51 |
+
|
| 52 |
+
Returns:
|
| 53 |
+
Normalized embedding, shape (4096,), L2 norm = 1.0
|
| 54 |
+
"""
|
| 55 |
+
if use_cache and claim in self.embedding_cache:
|
| 56 |
+
return self.embedding_cache[claim]
|
| 57 |
+
|
| 58 |
+
if self.model is None:
|
| 59 |
+
# Fallback for testing: deterministic dummy embedding
|
| 60 |
+
embedding = self._dummy_embedding(claim)
|
| 61 |
+
else:
|
| 62 |
+
try:
|
| 63 |
+
# Get final hidden states from Llama
|
| 64 |
+
hidden_state = self.model.encode(claim) # Shape: (dim,)
|
| 65 |
+
|
| 66 |
+
if hidden_state is None or len(hidden_state) == 0:
|
| 67 |
+
embedding = self._dummy_embedding(claim)
|
| 68 |
+
else:
|
| 69 |
+
embedding = np.array(hidden_state, dtype=np.float32)
|
| 70 |
+
except Exception as e:
|
| 71 |
+
print(f"Warning: Embedding failed for '{claim[:50]}...': {e}")
|
| 72 |
+
embedding = self._dummy_embedding(claim)
|
| 73 |
+
|
| 74 |
+
# Normalize L2
|
| 75 |
+
norm = np.linalg.norm(embedding)
|
| 76 |
+
if norm > 1e-8:
|
| 77 |
+
embedding = embedding / norm
|
| 78 |
+
else:
|
| 79 |
+
embedding = np.zeros_like(embedding)
|
| 80 |
+
|
| 81 |
+
if use_cache:
|
| 82 |
+
self.embedding_cache[claim] = embedding
|
| 83 |
+
|
| 84 |
+
return embedding
|
| 85 |
+
|
| 86 |
+
def _dummy_embedding(self, text: str) -> np.ndarray:
|
| 87 |
+
"""
|
| 88 |
+
Create deterministic dummy embedding from text for testing.
|
| 89 |
+
Not used in production, but allows testing without Llama.
|
| 90 |
+
"""
|
| 91 |
+
# Use text hash to seed RNG for reproducibility
|
| 92 |
+
seed = hash(text) % (2**31)
|
| 93 |
+
rng = np.random.RandomState(seed)
|
| 94 |
+
return rng.randn(self.embedding_dim).astype(np.float32)
|
| 95 |
+
|
| 96 |
+
def compute_semantic_tension(
|
| 97 |
+
self, claim_a: str, claim_b: str, return_components: bool = False
|
| 98 |
+
) -> float or Tuple[float, float]:
|
| 99 |
+
"""
|
| 100 |
+
Compute ξ_semantic = 1.0 - cosine_similarity(embed_a, embed_b).
|
| 101 |
+
|
| 102 |
+
Args:
|
| 103 |
+
claim_a: First claim text
|
| 104 |
+
claim_b: Second claim text
|
| 105 |
+
return_components: If True, also return similarity
|
| 106 |
+
|
| 107 |
+
Returns:
|
| 108 |
+
tension (float) in [0, 1], or (tension, similarity) if return_components
|
| 109 |
+
- 0.0 = identical claims (no tension)
|
| 110 |
+
- 0.5 = orthogonal claims (framework divergence)
|
| 111 |
+
- 1.0 = opposite claims (maximum tension)
|
| 112 |
+
"""
|
| 113 |
+
embed_a = self.embed_claim(claim_a)
|
| 114 |
+
embed_b = self.embed_claim(claim_b)
|
| 115 |
+
|
| 116 |
+
# Cosine similarity for normalized vectors = dot product
|
| 117 |
+
similarity = float(np.dot(embed_a, embed_b))
|
| 118 |
+
|
| 119 |
+
# Clamp to [-1, 1] in case of floating point errors
|
| 120 |
+
similarity = np.clip(similarity, -1.0, 1.0)
|
| 121 |
+
|
| 122 |
+
# Convert to tension: higher divergence = higher tension
|
| 123 |
+
# Formula: ξ = (1 - similarity) / 2 maps [-1, 1] similarity to [0, 1] tension
|
| 124 |
+
semantic_tension = (1.0 - similarity) / 2.0
|
| 125 |
+
|
| 126 |
+
if return_components:
|
| 127 |
+
return semantic_tension, similarity
|
| 128 |
+
return semantic_tension
|
| 129 |
+
|
| 130 |
+
def compute_polarity(self, claim_a: str, claim_b: str) -> str:
|
| 131 |
+
"""
|
| 132 |
+
Classify the relationship type between two claims using embeddings.
|
| 133 |
+
|
| 134 |
+
Logic:
|
| 135 |
+
- similarity > 0.7 : "paraphrase" (same meaning, different wording)
|
| 136 |
+
- similarity < -0.3 : "contradiction" (opposite meanings)
|
| 137 |
+
- -0.3 <= sim <= 0.7 : "framework" (orthogonal/different domains)
|
| 138 |
+
|
| 139 |
+
Returns:
|
| 140 |
+
polarity_type: "paraphrase" | "contradiction" | "framework"
|
| 141 |
+
"""
|
| 142 |
+
_, similarity = self.compute_semantic_tension(claim_a, claim_b, return_components=True)
|
| 143 |
+
|
| 144 |
+
if similarity > 0.7:
|
| 145 |
+
return "paraphrase"
|
| 146 |
+
elif similarity < -0.3:
|
| 147 |
+
return "contradiction"
|
| 148 |
+
else:
|
| 149 |
+
return "framework"
|
| 150 |
+
|
| 151 |
+
def explain_tension(self, claim_a: str, claim_b: str) -> Dict:
|
| 152 |
+
"""
|
| 153 |
+
Detailed breakdown of semantic tension for debugging/analysis.
|
| 154 |
+
|
| 155 |
+
Returns:
|
| 156 |
+
Dict with claims, tension, polarity, similarity, and raw embeddings
|
| 157 |
+
"""
|
| 158 |
+
embed_a = self.embed_claim(claim_a)
|
| 159 |
+
embed_b = self.embed_claim(claim_b)
|
| 160 |
+
|
| 161 |
+
tension, similarity = self.compute_semantic_tension(claim_a, claim_b, return_components=True)
|
| 162 |
+
polarity = self.compute_polarity(claim_a, claim_b)
|
| 163 |
+
|
| 164 |
+
return {
|
| 165 |
+
"claim_a": claim_a[:100],
|
| 166 |
+
"claim_b": claim_b[:100],
|
| 167 |
+
"semantic_tension": round(tension, 4),
|
| 168 |
+
"similarity": round(similarity, 4),
|
| 169 |
+
"polarity_type": polarity,
|
| 170 |
+
"embedding_a_norm": round(float(np.linalg.norm(embed_a)), 4),
|
| 171 |
+
"embedding_b_norm": round(float(np.linalg.norm(embed_b)), 4),
|
| 172 |
+
"embedding_dim": self.embedding_dim,
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
def compare_multiple(self, claims: list) -> Dict:
|
| 176 |
+
"""
|
| 177 |
+
Compare one claim against multiple others.
|
| 178 |
+
|
| 179 |
+
Useful for routing or measuring how divergent a set of claims is.
|
| 180 |
+
|
| 181 |
+
Args:
|
| 182 |
+
claims: List of claim strings
|
| 183 |
+
|
| 184 |
+
Returns:
|
| 185 |
+
{
|
| 186 |
+
"primary_claim": claims[0],
|
| 187 |
+
"pairwise_tensions": [
|
| 188 |
+
{"claim": "...", "tension": 0.35, "polarity": "framework"}
|
| 189 |
+
],
|
| 190 |
+
"mean_tension": 0.42,
|
| 191 |
+
"max_tension": 0.78,
|
| 192 |
+
}
|
| 193 |
+
"""
|
| 194 |
+
if len(claims) < 2:
|
| 195 |
+
return {"error": "need at least 2 claims"}
|
| 196 |
+
|
| 197 |
+
primary = claims[0]
|
| 198 |
+
comparisons = []
|
| 199 |
+
|
| 200 |
+
for claim in claims[1:]:
|
| 201 |
+
tension = self.compute_semantic_tension(primary, claim)
|
| 202 |
+
polarity = self.compute_polarity(primary, claim)
|
| 203 |
+
comparisons.append({
|
| 204 |
+
"claim": claim[:100],
|
| 205 |
+
"tension": round(tension, 4),
|
| 206 |
+
"polarity": polarity,
|
| 207 |
+
})
|
| 208 |
+
|
| 209 |
+
mean_tension = float(np.mean([c["tension"] for c in comparisons]))
|
| 210 |
+
max_tension = float(np.max([c["tension"] for c in comparisons]))
|
| 211 |
+
|
| 212 |
+
return {
|
| 213 |
+
"primary_claim": primary[:100],
|
| 214 |
+
"pairwise_tensions": comparisons,
|
| 215 |
+
"mean_tension": round(mean_tension, 4),
|
| 216 |
+
"max_tension": round(max_tension, 4),
|
| 217 |
+
"num_compared": len(comparisons),
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
def clear_cache(self):
|
| 221 |
+
"""Clear embedding cache to free memory."""
|
| 222 |
+
self.embedding_cache.clear()
|
| 223 |
+
|
| 224 |
+
def get_cache_stats(self) -> Dict:
|
| 225 |
+
"""Get embedding cache statistics."""
|
| 226 |
+
return {
|
| 227 |
+
"cached_embeddings": len(self.embedding_cache),
|
| 228 |
+
"embedding_dim": self.embedding_dim,
|
| 229 |
+
"approximate_cache_size_mb": (len(self.embedding_cache) * self.embedding_dim * 4) / (1024 ** 2),
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
# Export for use in conflict_engine.py and other modules
|
| 234 |
+
__all__ = ["SemanticTensionEngine"]
|
reasoning_forge/specialization_tracker.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Phase 6: Specialization Tracker
|
| 3 |
+
|
| 4 |
+
Monitors adapter specialization and prevents semantic convergence.
|
| 5 |
+
|
| 6 |
+
Key metrics:
|
| 7 |
+
- specialization_score = domain_accuracy / usage_frequency
|
| 8 |
+
(higher = expert in domain, not overtaxed)
|
| 9 |
+
- semantic_convergence = similarity between adapter outputs
|
| 10 |
+
(alert if > 0.85, indicates monoculture within adapters)
|
| 11 |
+
|
| 12 |
+
Prevents:
|
| 13 |
+
- Weight drift (Phase 5 catches at system level)
|
| 14 |
+
- Semantic convergence (adapters giving similar answers, Phase 6 catches)
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from typing import List, Dict, Optional
|
| 18 |
+
import numpy as np
|
| 19 |
+
from datetime import datetime
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class SpecializationTracker:
|
| 23 |
+
"""
|
| 24 |
+
Tracks per-adapter per-domain performance to maintain specialization
|
| 25 |
+
and detect when adapters are overlapping semantically.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
# Domain keywords for query classification
|
| 29 |
+
DOMAIN_KEYWORDS = {
|
| 30 |
+
"physics": ["force", "momentum", "gravity", "quantum", "relativity", "acceleration", "Newton", "energy"],
|
| 31 |
+
"ethics": ["should", "right", "wrong", "moral", "ethics", "justice", "fair", "values", "good"],
|
| 32 |
+
"consciousness": ["aware", "conscious", "mind", "self", "experience", "perception", "qualia", "sentient"],
|
| 33 |
+
"creativity": ["design", "create", "novel", "innovative", "imagine", "artistic", "original", "aesthetic"],
|
| 34 |
+
"systems": ["system", "architecture", "scalable", "complex", "interdependent", "emergence", "network"],
|
| 35 |
+
"philosophy": ["meaning", "existence", "truth", "knowledge", "being", "essence", "reasoning"],
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
def __init__(self):
|
| 39 |
+
"""Initialize tracking dictionaries."""
|
| 40 |
+
self.domain_accuracy = {} # {adapter: {domain: [coherence_scores]}}
|
| 41 |
+
self.domain_usage = {} # {adapter: {domain: count}}
|
| 42 |
+
self.domain_last_used = {} # {adapter: {domain: timestamp}}
|
| 43 |
+
self.query_domains = {} # {query_id: [domain_tags]}
|
| 44 |
+
self.semantic_convergence_history = [] # Track convergence over time
|
| 45 |
+
|
| 46 |
+
def classify_query_domain(self, query: str) -> List[str]:
|
| 47 |
+
"""
|
| 48 |
+
Classify query by topic domain using keyword heuristics.
|
| 49 |
+
|
| 50 |
+
Returns:
|
| 51 |
+
List of domain tags, e.g., ["physics", "ethics"] for multi-domain queries.
|
| 52 |
+
Returns ["general"] if no keywords match.
|
| 53 |
+
"""
|
| 54 |
+
domains = []
|
| 55 |
+
query_lower = query.lower()
|
| 56 |
+
|
| 57 |
+
for domain, keywords in self.DOMAIN_KEYWORDS.items():
|
| 58 |
+
if any(k.lower() in query_lower for k in keywords):
|
| 59 |
+
domains.append(domain)
|
| 60 |
+
|
| 61 |
+
return domains if domains else ["general"]
|
| 62 |
+
|
| 63 |
+
def record_adapter_performance(self, adapter: str, query: str, coherence: float):
|
| 64 |
+
"""
|
| 65 |
+
Log adapter performance in domain(s) for a query.
|
| 66 |
+
|
| 67 |
+
Args:
|
| 68 |
+
adapter: Adapter name (e.g., "newton", "empathy")
|
| 69 |
+
query: Query text
|
| 70 |
+
coherence: Output coherence score [0, 1]
|
| 71 |
+
"""
|
| 72 |
+
domains = self.classify_query_domain(query)
|
| 73 |
+
|
| 74 |
+
for domain in domains:
|
| 75 |
+
# Initialize if needed
|
| 76 |
+
if adapter not in self.domain_accuracy:
|
| 77 |
+
self.domain_accuracy[adapter] = {}
|
| 78 |
+
self.domain_usage[adapter] = {}
|
| 79 |
+
self.domain_last_used[adapter] = {}
|
| 80 |
+
|
| 81 |
+
if domain not in self.domain_accuracy[adapter]:
|
| 82 |
+
self.domain_accuracy[adapter][domain] = []
|
| 83 |
+
self.domain_usage[adapter][domain] = 0
|
| 84 |
+
self.domain_last_used[adapter][domain] = None
|
| 85 |
+
|
| 86 |
+
# Record coherence and increment usage
|
| 87 |
+
self.domain_accuracy[adapter][domain].append(coherence)
|
| 88 |
+
self.domain_usage[adapter][domain] += 1
|
| 89 |
+
self.domain_last_used[adapter][domain] = datetime.now()
|
| 90 |
+
|
| 91 |
+
def compute_specialization(self, adapter: str) -> Dict[str, float]:
|
| 92 |
+
"""
|
| 93 |
+
Compute specialization_score for each domain an adapter is used in.
|
| 94 |
+
|
| 95 |
+
specialization_score[domain] = mean_accuracy[domain] / usage_frequency[domain]
|
| 96 |
+
|
| 97 |
+
Returns:
|
| 98 |
+
{domain: specialization_score} for all domains used
|
| 99 |
+
Higher = more specialized (good performance, not overused)
|
| 100 |
+
"""
|
| 101 |
+
if adapter not in self.domain_accuracy:
|
| 102 |
+
return {}
|
| 103 |
+
|
| 104 |
+
specialization = {}
|
| 105 |
+
|
| 106 |
+
for domain in self.domain_accuracy[adapter]:
|
| 107 |
+
accuracies = self.domain_accuracy[adapter][domain]
|
| 108 |
+
usage = self.domain_usage[adapter][domain]
|
| 109 |
+
|
| 110 |
+
mean_accuracy = float(np.mean(accuracies)) if accuracies else 0.5
|
| 111 |
+
# Avoid division by zero, natural penalty for high usage
|
| 112 |
+
specialization[domain] = mean_accuracy / max(usage, 1)
|
| 113 |
+
|
| 114 |
+
return specialization
|
| 115 |
+
|
| 116 |
+
def get_global_specialization(self) -> Dict[str, Dict[str, float]]:
|
| 117 |
+
"""
|
| 118 |
+
Compute specialization scores for all adapters.
|
| 119 |
+
|
| 120 |
+
Returns:
|
| 121 |
+
{adapter: {domain: specialization_score}}
|
| 122 |
+
"""
|
| 123 |
+
return {adapter: self.compute_specialization(adapter) for adapter in self.domain_accuracy.keys()}
|
| 124 |
+
|
| 125 |
+
def detect_domain_expert(self, domain: str) -> Optional[str]:
|
| 126 |
+
"""
|
| 127 |
+
Find best-performing adapter for a specific domain.
|
| 128 |
+
|
| 129 |
+
Returns:
|
| 130 |
+
Adapter name with highest specialization in domain, or None
|
| 131 |
+
"""
|
| 132 |
+
specs = self.get_global_specialization()
|
| 133 |
+
experts = {a: s.get(domain, 0) for a, s in specs.items() if domain in s}
|
| 134 |
+
|
| 135 |
+
if not experts:
|
| 136 |
+
return None
|
| 137 |
+
|
| 138 |
+
return max(experts.keys(), key=lambda a: experts[a])
|
| 139 |
+
|
| 140 |
+
def detect_semantic_convergence(
|
| 141 |
+
self, adapter_outputs: Dict[str, str], semantic_engine=None, threshold: float = 0.85
|
| 142 |
+
) -> Dict:
|
| 143 |
+
"""
|
| 144 |
+
Measure overlap between adapter outputs on same query.
|
| 145 |
+
|
| 146 |
+
Alerts if any pair similarity > threshold (converging).
|
| 147 |
+
|
| 148 |
+
Args:
|
| 149 |
+
adapter_outputs: {adapter_name: output_text}
|
| 150 |
+
semantic_engine: SemanticTensionEngine instance (optional, for real embeddings)
|
| 151 |
+
threshold: Similarity threshold for convergence alert
|
| 152 |
+
|
| 153 |
+
Returns:
|
| 154 |
+
{
|
| 155 |
+
"convergent_pairs": [{pair, similarity, risk}],
|
| 156 |
+
"max_similarity": float,
|
| 157 |
+
"has_convergence": bool,
|
| 158 |
+
}
|
| 159 |
+
"""
|
| 160 |
+
if len(adapter_outputs) < 2:
|
| 161 |
+
return {"convergent_pairs": [], "max_similarity": 0.0, "has_convergence": False}
|
| 162 |
+
|
| 163 |
+
convergent_pairs = []
|
| 164 |
+
max_similarity = 0.0
|
| 165 |
+
|
| 166 |
+
adapters = list(adapter_outputs.keys())
|
| 167 |
+
|
| 168 |
+
for i, a1 in enumerate(adapters):
|
| 169 |
+
for a2 in adapters[i + 1 :]:
|
| 170 |
+
output_a = adapter_outputs[a1]
|
| 171 |
+
output_b = adapter_outputs[a2]
|
| 172 |
+
|
| 173 |
+
# Compute similarity (use semantic engine if available)
|
| 174 |
+
if semantic_engine:
|
| 175 |
+
try:
|
| 176 |
+
tension = semantic_engine.compute_semantic_tension(output_a, output_b)
|
| 177 |
+
similarity = 1.0 - tension
|
| 178 |
+
except Exception:
|
| 179 |
+
# Fallback to text overlap
|
| 180 |
+
similarity = self._text_similarity(output_a, output_b)
|
| 181 |
+
else:
|
| 182 |
+
# Simple fallback: token overlap
|
| 183 |
+
similarity = self._text_similarity(output_a, output_b)
|
| 184 |
+
|
| 185 |
+
max_similarity = max(max_similarity, similarity)
|
| 186 |
+
|
| 187 |
+
if similarity > threshold:
|
| 188 |
+
convergent_pairs.append({
|
| 189 |
+
"adapter_a": a1,
|
| 190 |
+
"adapter_b": a2,
|
| 191 |
+
"similarity": round(similarity, 3),
|
| 192 |
+
"convergence_risk": "HIGH" if similarity > 0.92 else "MEDIUM",
|
| 193 |
+
})
|
| 194 |
+
|
| 195 |
+
has_convergence = len(convergent_pairs) > 0
|
| 196 |
+
|
| 197 |
+
record = {
|
| 198 |
+
"timestamp": datetime.now().isoformat(),
|
| 199 |
+
"convergent_pairs": convergent_pairs,
|
| 200 |
+
"max_similarity": round(max_similarity, 3),
|
| 201 |
+
"has_convergence": has_convergence,
|
| 202 |
+
"num_adapters": len(adapter_outputs),
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
self.semantic_convergence_history.append(record)
|
| 206 |
+
|
| 207 |
+
return record
|
| 208 |
+
|
| 209 |
+
def _text_similarity(self, text_a: str, text_b: str) -> float:
|
| 210 |
+
"""
|
| 211 |
+
Simple text similarity fallback: Jaccard similarity on tokens.
|
| 212 |
+
|
| 213 |
+
Args:
|
| 214 |
+
text_a, text_b: Text strings
|
| 215 |
+
|
| 216 |
+
Returns:
|
| 217 |
+
Similarity in [0, 1]
|
| 218 |
+
"""
|
| 219 |
+
tokens_a = set(text_a.lower().split())
|
| 220 |
+
tokens_b = set(text_b.lower().split())
|
| 221 |
+
|
| 222 |
+
if not tokens_a or not tokens_b:
|
| 223 |
+
return 0.0
|
| 224 |
+
|
| 225 |
+
intersection = len(tokens_a & tokens_b)
|
| 226 |
+
union = len(tokens_a | tokens_b)
|
| 227 |
+
|
| 228 |
+
return intersection / max(union, 1)
|
| 229 |
+
|
| 230 |
+
def get_adapter_health(self, adapter: str) -> Dict:
|
| 231 |
+
"""
|
| 232 |
+
Get overall health score for an adapter.
|
| 233 |
+
|
| 234 |
+
Returns:
|
| 235 |
+
{
|
| 236 |
+
"adapter": adapter,
|
| 237 |
+
"num_domains": int,
|
| 238 |
+
"avg_accuracy": float,
|
| 239 |
+
"total_usage": int,
|
| 240 |
+
"specialization_avg": float,
|
| 241 |
+
"recommendation": str
|
| 242 |
+
}
|
| 243 |
+
"""
|
| 244 |
+
if adapter not in self.domain_accuracy:
|
| 245 |
+
return {"error": f"No data for adapter {adapter}"}
|
| 246 |
+
|
| 247 |
+
accuracies_all = []
|
| 248 |
+
usage_total = 0
|
| 249 |
+
|
| 250 |
+
for domain in self.domain_accuracy[adapter]:
|
| 251 |
+
accuracies_all.extend(self.domain_accuracy[adapter][domain])
|
| 252 |
+
usage_total += self.domain_usage[adapter][domain]
|
| 253 |
+
|
| 254 |
+
avg_accuracy = float(np.mean(accuracies_all)) if accuracies_all else 0.5
|
| 255 |
+
specs = self.compute_specialization(adapter)
|
| 256 |
+
spec_avg = float(np.mean(list(specs.values()))) if specs else 0.5
|
| 257 |
+
|
| 258 |
+
# Generate recommendation
|
| 259 |
+
if spec_avg > 0.1 and avg_accuracy > 0.75:
|
| 260 |
+
recommendation = "excellent_specialist"
|
| 261 |
+
elif spec_avg > 0.05 and avg_accuracy > 0.6:
|
| 262 |
+
recommendation = "good_generalist"
|
| 263 |
+
elif usage_total > 20 and avg_accuracy < 0.5:
|
| 264 |
+
recommendation = "overused_poorly"
|
| 265 |
+
else:
|
| 266 |
+
recommendation = "maintain_current"
|
| 267 |
+
|
| 268 |
+
return {
|
| 269 |
+
"adapter": adapter,
|
| 270 |
+
"num_domains": len(self.domain_accuracy[adapter]),
|
| 271 |
+
"avg_accuracy": round(avg_accuracy, 3),
|
| 272 |
+
"total_usage": usage_total,
|
| 273 |
+
"specialization_avg": round(spec_avg, 3),
|
| 274 |
+
"recommendation": recommendation,
|
| 275 |
+
"domain_specializations": {d: round(s, 3) for d, s in specs.items()},
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
def get_system_health(self) -> Dict:
|
| 279 |
+
"""
|
| 280 |
+
Get overall system specialization health.
|
| 281 |
+
|
| 282 |
+
Returns:
|
| 283 |
+
Flags convergence risks, identifies experts, recommends actions.
|
| 284 |
+
"""
|
| 285 |
+
health_by_adapter = {adapter: self.get_adapter_health(adapter) for adapter in self.domain_accuracy.keys()}
|
| 286 |
+
|
| 287 |
+
overused = [a for a, h in health_by_adapter.items() if h.get("recommendation") == "overused_poorly"]
|
| 288 |
+
excellent = [a for a, h in health_by_adapter.items() if h.get("recommendation") == "excellent_specialist"]
|
| 289 |
+
experts = {domain: self.detect_domain_expert(domain) for domain in self.DOMAIN_KEYWORDS.keys()}
|
| 290 |
+
|
| 291 |
+
return {
|
| 292 |
+
"timestamp": datetime.now().isoformat(),
|
| 293 |
+
"total_adapters": len(health_by_adapter),
|
| 294 |
+
"health_by_adapter": health_by_adapter,
|
| 295 |
+
"overused_adapters": overused,
|
| 296 |
+
"specialist_adapters": excellent,
|
| 297 |
+
"domain_experts": experts,
|
| 298 |
+
"convergence_alerts": self.semantic_convergence_history[-5:] if self.semantic_convergence_history else [],
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
def export_summary(self) -> Dict:
|
| 302 |
+
"""Export complete specialization data for analysis."""
|
| 303 |
+
return {
|
| 304 |
+
"timestamp": datetime.now().isoformat(),
|
| 305 |
+
"global_specialization": self.get_global_specialization(),
|
| 306 |
+
"system_health": self.get_system_health(),
|
| 307 |
+
"convergence_history": self.semantic_convergence_history,
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
__all__ = ["SpecializationTracker"]
|