Raiff1982 commited on
Commit
e026d7f
·
1 Parent(s): ab1515b

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 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"]