repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | """ |
| #2 | Self-Harmonizing Memory Reasoning (SHMR) |
| #3 | ======================================== |
| #4 | Built on ECHO-OR research (AxDSan/ECHO-OR) but fully rearchitected for |
| #5 | continuous local memory orchestration inside Mnemosyne's BEAM architecture. |
| #6 | |
| #7 | Core idea: related memories "echo" each other in the background, negotiating |
| #8 | contradictions, surfacing hidden patterns, and converging into stable beliefs. |
| #9 | |
| #10 | This is Mnemosyne's signature reasoning layer -- no Honcho dreams, no Hindsight |
| #11 | reflections, no Mem0 static graphs. Memories actively resonate and self-correct. |
| #12 | """ |
| #13 | |
| #14 | import os |
| #15 | import time |
| #16 | import logging |
| #17 | import json |
| #18 | from typing import List, Dict, Optional, Tuple |
| #19 | from pathlib import Path |
| #20 | |
| #21 | import numpy as np |
| #22 | |
| #23 | from mnemosyne.core import embeddings as _embeddings |
| #24 | |
| #25 | logger = logging.getLogger("mnemosyne.shmr") |
| #26 | |
| #27 | # --- Config --- |
| #28 | SHMR_BATCH_SIZE = int(os.environ.get("MNEMOSYNE_SHMR_BATCH_SIZE", "50")) |
| #29 | SHMR_MAX_ITERATIONS = int(os.environ.get("MNEMOSYNE_SHMR_MAX_ITERATIONS", "3")) |
| #30 | SHMR_SIMILARITY_THRESHOLD = float(os.environ.get("MNEMOSYNE_SHMR_SIMILARITY_THRESHOLD", "0.70")) |
| #31 | SHMR_HARMONY_THRESHOLD = float(os.environ.get("MNEMOSYNE_SHMR_HARMONY_THRESHOLD", "0.60")) |
| #32 | SHMR_MODEL = os.environ.get("MNEMOSYNE_SHMR_MODEL", "") |
| #33 | SHMR_MIN_CLUSTER_SIZE = int(os.environ.get("MNEMOSYNE_SHMR_MIN_CLUSTER_SIZE", "2")) |
| #34 | SHMR_TEMPERATURE = float(os.environ.get("MNEMOSYNE_SHMR_TEMPERATURE", "0.2")) |
| #35 | |
| #36 | EMBEDDING_DIM = 384 # bge-small-en-v1.5 |
| #37 | |
| #38 | # --- SQL Schema --- |
| #39 | FACTS_SCHEMA_SQL = """ |
| #40 | CREATE TABLE IF NOT EXISTS harmonic_beliefs ( |
| #41 | belief_id TEXT PRIMARY KEY, |
| #42 | subject TEXT, |
| #43 | predicate TEXT, |
| #44 | object TEXT NOT NULL, |
| #45 | confidence REAL DEFAULT 0.5, |
| #46 | provenance TEXT, -- JSON array of source fact_ids or memory_ids |
| #47 | cluster_id TEXT, |
| #48 | iteration INTEGER DEFAULT 0, |
| #49 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| #50 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
| #51 | ); |
| #52 | |
| #53 | CREATE TABLE IF NOT EXISTS memory_resonance_log ( |
| #54 | id INTEGER PRIMARY KEY AUTOINCREMENT, |
| #55 | session_id TEXT, |
| #56 | cluster_count INTEGER, |
| #57 | beliefs_generated INTEGER, |
| #58 | contradictions_resolved INTEGER, |
| #59 | harmony_score_avg REAL, |
| #60 | duration_ms INTEGER, |
| #61 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
| #62 | ); |
| #63 | |
| #64 | CREATE INDEX IF NOT EXISTS idx_beliefs_subject ON harmonic_beliefs(subject); |
| #65 | CREATE INDEX IF NOT EXISTS idx_beliefs_predicate ON harmonic_beliefs(predicate); |
| #66 | CREATE INDEX IF NOT EXISTS idx_beliefs_confidence ON harmonic_beliefs(confidence); |
| #67 | """ |
| #68 | |
| #69 | |
| #70 | def _init_schema(conn): |
| #71 | """Ensure SHMR tables exist.""" |
| #72 | conn.executescript(FACTS_SCHEMA_SQL) |
| #73 | conn.commit() |
| #74 | |
| #75 | |
| #76 | def _embed(text: str) -> np.ndarray: |
| #77 | """Embed text using Mnemosyne's embedding pipeline (BAAI/bge-small).""" |
| #78 | emb = _embeddings.embed(text) |
| #79 | if emb.ndim > 1: |
| #80 | emb = emb.flatten() |
| #81 | return emb.astype(np.float32) |
| #82 | |
| #83 | |
| #84 | def _cosine_similarity(a: np.ndarray, b: np.ndarray) -> float: |
| #85 | """Cosine similarity between two normalized vectors.""" |
| #86 | a_norm = a / (np.linalg.norm(a) + 1e-8) |
| #87 | b_norm = b / (np.linalg.norm(b) + 1e-8) |
| #88 | return float(np.dot(a_norm, b_norm)) |
| #89 | |
| #90 | |
| #91 | def _cluster_by_similarity( |
| #92 | items: List[Dict], |
| #93 | threshold: float, |
| #94 | ) -> List[List[Dict]]: |
| #95 | """Greedy connected-components clustering by cosine similarity. |
| #96 | |
| #97 | Each item must have an 'embedding' key with a numpy array. |
| #98 | Returns list of clusters (each cluster is a list of items). |
| #99 | """ |
| #100 | if not items: |
| #101 | return [] |
| #102 | |
| #103 | n = len(items) |
| #104 | # Build adjacency: items are connected if sim >= threshold |
| #105 | adj = {i: set() for i in range(n)} |
| #106 | for i in range(n): |
| #107 | for j in range(i + 1, n): |
| #108 | sim = _cosine_similarity(items[i]["embedding"], items[j]["embedding"]) |
| #109 | if sim >= threshold: |
| #110 | adj[i].add(j) |
| #111 | adj[j].add(i) |
| #112 | |
| #113 | # Connected components (BFS) |
| #114 | visited = set() |
| #115 | clusters = [] |
| #116 | for i in range(n): |
| #117 | if i in visited: |
| #118 | continue |
| #119 | cluster = [] |
| #120 | stack = [i] |
| #121 | while stack: |
| #122 | node = stack.pop() |
| #123 | if node in visited: |
| #124 | continue |
| #125 | visited.add(node) |
| #126 | cluster.append(items[node]) |
| #127 | stack.extend(adj[node] - visited) |
| #128 | clusters.append(cluster) |
| #129 | |
| #130 | return clusters |
| #131 | |
| #132 | |
| #133 | def _format_cluster_for_llm(cluster: List[Dict]) -> str: |
| #134 | """Format a memory cluster as a prompt for the LLM harmonizer.""" |
| #135 | lines = ["=== MEMORY CLUSTER ==="] |
| #136 | for i, item in enumerate(cluster): |
| #137 | subject = item.get("subject", "unknown") |
| #138 | predicate = item.get("predicate", "stated") |
| #139 | obj = item.get("object", item.get("content", "")) |
| #140 | confidence = item.get("confidence", 0.5) |
| #141 | timestamp = item.get("timestamp", "unknown") |
| #142 | source = item.get("source", "fact") |
| #143 | lines.append( |
| #144 | f"[{i}] ({source}, conf={confidence:.2f}) {subject} | {predicate} | {obj}" |
| #145 | ) |
| #146 | return "\n".join(lines) |
| #147 | |
| #148 | |
| #149 | HARMONY_PROMPT = """You are the Self-Harmonizing Memory Reasoner for Mnemosyne. |
| #150 | These memories belong to the same semantic cluster -- they all relate to the |
| #151 | same entities, topics, or events. Your job is to harmonize them: |
| #152 | |
| #153 | 1. **Resolve contradictions**: If two memories conflict, determine which is more |
| #154 | likely true based on recency, specificity, and internal consistency. Flag the |
| #155 | weaker one as dampened, not deleted. |
| #156 | 2. **Extract higher-order beliefs**: Find patterns that span multiple memories. |
| #157 | What does this cluster as a whole tell us? What's the stable truth? |
| #158 | 3. **Dampen noise, amplify signal**: Low-confidence or stale memories get lower |
| #159 | weight. Corroborated facts get reinforced. |
| #160 | 4. **Output only stable beliefs**: Return NEW or UPDATED facts with confidence |
| #161 | scores. Don't regurgitate every input fact -- synthesize. |
| #162 | |
| #163 | Output as JSON array of belief objects: |
| #164 | [{"subject": "...", "predicate": "...", "object": "...", "confidence": 0.0-1.0, |
| #165 | "action": "create"|"update"|"dampen", "target_fact_id": null|"fact_id", |
| #166 | "rationale": "one sentence explaining why"}] |
| #167 | |
| #168 | RULES: |
| #169 | - Confidence 0.9+ = highly corroborated (multiple sources agree) |
| #170 | - Confidence 0.5-0.8 = reasonable inference from the cluster |
| #171 | - Confidence <0.4 = speculative, mark as such |
| #172 | - Use "dampen" to reduce confidence of contradicted facts (never delete) |
| #173 | - Use "update" to modify an existing fact with new information |
| #174 | - Output 1-5 beliefs per cluster (don't over-generate)""" |
| #175 | |
| #176 | |
| #177 | def _call_llm(prompt: str, system: str = "") -> str: |
| #178 | """Call the configured LLM for harmonization. |
| #179 | |
| #180 | Uses the same LLM chain as mnemosyne_sleep's summarization: |
| #181 | local_llm first, fallback to cloud extraction client. |
| #182 | """ |
| #183 | # Try local LLM first |
| #184 | try: |
| #185 | from mnemosyne.core.local_llm import _call_local_llm |
| #186 | result = _call_local_llm(prompt, system=system, temperature=SHMR_TEMPERATURE) |
| #187 | if result and len(result.strip()) > 10: |
| #188 | return result |
| #189 | except Exception: |
| #190 | pass |
| #191 | |
| #192 | # Fallback to cloud extraction client |
| #193 | try: |
| #194 | from mnemosyne.core.extraction import ExtractionConfig, ExtractionClient |
| #195 | config = ExtractionConfig() |
| #196 | client = ExtractionClient(config) |
| #197 | messages = [] |
| #198 | if system: |
| #199 | messages.append({"role": "system", "content": system}) |
| #200 | messages.append({"role": "user", "content": prompt}) |
| #201 | result = client.chat(messages, temperature=SHMR_TEMPERATURE) |
| #202 | if result: |
| #203 | return result |
| #204 | except Exception: |
| #205 | pass |
| #206 | |
| #207 | return "" |
| #208 | |
| #209 | |
| #210 | def _compute_harmony_score( |
| #211 | beliefs: List[Dict], |
| #212 | cluster: List[Dict], |
| #213 | ) -> float: |
| #214 | """Score how well the harmonized beliefs represent the cluster. |
| #215 | |
| #216 | Uses cosine similarity between belief embeddings and cluster centroid, |
| #217 | plus a consistency bonus for beliefs that don't contradict each other. |
| #218 | """ |
| #219 | if not beliefs or not cluster: |
| #220 | return 0.0 |
| #221 | |
| #222 | # Compute cluster centroid |
| #223 | cluster_embs = np.array([item.get("embedding", np.zeros(EMBEDDING_DIM)) |
| #224 | for item in cluster]) |
| #225 | centroid = cluster_embs.mean(axis=0) |
| #226 | |
| #227 | # Score each belief against centroid |
| #228 | belief_scores = [] |
| #229 | for belief in beliefs: |
| #230 | belief_text = f"{belief.get('predicate', '')} {belief.get('object', '')}" |
| #231 | try: |
| #232 | belief_emb = _embed(belief_text) |
| #233 | sim = _cosine_similarity(belief_emb, centroid) |
| #234 | belief_scores.append(sim * belief.get("confidence", 0.5)) |
| #235 | except Exception: |
| #236 | belief_scores.append(0.3) |
| #237 | |
| #238 | # Consistency bonus: penalize if beliefs contradict each other |
| #239 | consistency_bonus = 1.0 |
| #240 | if len(beliefs) > 1: |
| #241 | belief_embs = [] |
| #242 | for b in beliefs: |
| #243 | try: |
| #244 | belief_embs.append(_embed(f"{b.get('predicate','')} {b.get('object','')}")) |
| #245 | except Exception: |
| #246 | belief_embs.append(np.zeros(EMBEDDING_DIM)) |
| #247 | belief_embs = np.array(belief_embs) |
| #248 | |
| #249 | # Check pairwise similarity of beliefs (if they're too different, |
| #250 | # that suggests the LLM produced contradictory beliefs) |
| #251 | pairwise_sims = [] |
| #252 | for i in range(len(belief_embs)): |
| #253 | for j in range(i + 1, len(belief_embs)): |
| #254 | pairwise_sims.append(_cosine_similarity(belief_embs[i], belief_embs[j])) |
| #255 | if pairwise_sims: |
| #256 | avg_pairwise = np.mean(pairwise_sims) |
| #257 | # Lower pairwise similarity = potential contradiction = penalty |
| #258 | consistency_bonus = min(1.0, avg_pairwise + 0.3) |
| #259 | |
| #260 | avg_belief_score = np.mean(belief_scores) if belief_scores else 0.0 |
| #261 | return float(avg_belief_score * consistency_bonus) |
| #262 | |
| #263 | |
| #264 | def _extract_json_from_llm_output(text: str) -> List[Dict]: |
| #265 | """Robust JSON extraction from LLM output (handles markdown wrappers).""" |
| #266 | import re |
| #267 | |
| #268 | # Try direct parse first |
| #269 | try: |
| #270 | parsed = json.loads(text) |
| #271 | if isinstance(parsed, list): |
| #272 | return parsed |
| #273 | if isinstance(parsed, dict) and "beliefs" in parsed: |
| #274 | return parsed["beliefs"] |
| #275 | except (json.JSONDecodeError, TypeError): |
| #276 | pass |
| #277 | |
| #278 | # Try extracting from ```json ... ``` block |
| #279 | json_match = re.search(r'```(?:json)?\s*(\[.*?\])\s*```', text, re.DOTALL) |
| #280 | if json_match: |
| #281 | try: |
| #282 | return json.loads(json_match.group(1)) |
| #283 | except (json.JSONDecodeError, TypeError): |
| #284 | pass |
| #285 | |
| #286 | # Try extracting bare array |
| #287 | array_match = re.search(r'\[\s*\{.*?\}\s*\]', text, re.DOTALL) |
| #288 | if array_match: |
| #289 | try: |
| #290 | return json.loads(array_match.group(0)) |
| #291 | except (json.JSONDecodeError, TypeError): |
| #292 | pass |
| #293 | |
| #294 | # Fallback: parse line by line for { ... } objects |
| #295 | objects = re.findall(r'\{[^{}]*\}', text) |
| #296 | results = [] |
| #297 | for obj_str in objects: |
| #298 | try: |
| #299 | results.append(json.loads(obj_str)) |
| #300 | except (json.JSONDecodeError, TypeError): |
| #301 | continue |
| #302 | return results |
| #303 | |
| #304 | |
| #305 | def _apply_beliefs(conn, beliefs: List[Dict], cluster: List[Dict], cluster_id: str): |
| #306 | """Write harmonized beliefs to the database and update source facts.""" |
| #307 | import hashlib |
| #308 | cursor = conn.cursor() |
| #309 | now = __import__("datetime").datetime.now().isoformat() |
| #310 | |
| #311 | for belief in beliefs: |
| #312 | action = belief.get("action", "create") |
| #313 | subject = belief.get("subject", "entity") |
| #314 | predicate = belief.get("predicate", "related_to") |
| #315 | obj = belief.get("object", "") |
| #316 | confidence = max(0.1, min(1.0, belief.get("confidence", 0.5))) |
| #317 | |
| #318 | belief_id = hashlib.sha256( |
| #319 | f"{cluster_id}:{subject}:{predicate}:{obj[:50]}".encode() |
| #320 | ).hexdigest()[:24] |
| #321 | |
| #322 | if action == "dampen": |
| #323 | target_id = belief.get("target_fact_id") |
| #324 | if target_id: |
| #325 | cursor.execute( |
| #326 | "UPDATE facts SET confidence = MAX(0.1, confidence - 0.15) WHERE fact_id = ?", |
| #327 | (target_id,) |
| #328 | ) |
| #329 | |
| #330 | elif action == "update": |
| #331 | target_id = belief.get("target_fact_id") |
| #332 | if target_id: |
| #333 | cursor.execute( |
| #334 | "UPDATE facts SET object = ?, confidence = ? WHERE fact_id = ?", |
| #335 | (obj, confidence, target_id) |
| #336 | ) |
| #337 | |
| #338 | # Always store the belief (create/update both produce a belief) |
| #339 | try: |
| #340 | cursor.execute(""" |
| #341 | INSERT OR REPLACE INTO harmonic_beliefs |
| #342 | (belief_id, subject, predicate, object, confidence, |
| #343 | provenance, cluster_id, iteration, updated_at) |
| #344 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) |
| #345 | """, ( |
| #346 | belief_id, subject, predicate, obj, confidence, |
| #347 | json.dumps([c.get("fact_id", "") for c in cluster if c.get("fact_id")]), |
| #348 | cluster_id, 0, now |
| #349 | )) |
| #350 | except Exception: |
| #351 | continue |
| #352 | |
| #353 | conn.commit() |
| #354 | |
| #355 | |
| #356 | def harmonize(beam, batch_size: int = None, max_iterations: int = None, |
| #357 | similarity_threshold: float = None) -> Dict: |
| #358 | """Run one harmonic cycle over recent memories. |
| #359 | |
| #360 | Called automatically by mnemosyne_sleep() after consolidation. |
| #361 | Can also be called directly via MCP tool for on-demand harmonization. |
| #362 | |
| #363 | Args: |
| #364 | beam: BeamMemory instance |
| #365 | batch_size: Max memories to process (default: MNEMOSYNE_SHMR_BATCH_SIZE) |
| #366 | max_iterations: Refinement iterations per cluster (default: 3) |
| #367 | similarity_threshold: Cosine threshold for clustering (default: 0.70) |
| #368 | |
| #369 | Returns: |
| #370 | Dict with stats: clusters_found, beliefs_generated, contradictions_resolved, |
| #371 | harmony_score_avg, duration_ms |
| #372 | """ |
| #373 | if batch_size is None: |
| #374 | batch_size = SHMR_BATCH_SIZE |
| #375 | if max_iterations is None: |
| #376 | max_iterations = SHMR_MAX_ITERATIONS |
| #377 | if similarity_threshold is None: |
| #378 | similarity_threshold = SHMR_SIMILARITY_THRESHOLD |
| #379 | |
| #380 | t0 = time.perf_counter() |
| #381 | _init_schema(beam.conn) |
| #382 | cursor = beam.conn.cursor() |
| #383 | |
| #384 | # --- Step 1: Pull echo candidates --- |
| #385 | # Prioritize: recent facts + high-confidence episodic memories |
| #386 | candidates = [] |
| #387 | |
| #388 | # Facts (status = active or NULL) |
| #389 | fact_rows = cursor.execute(""" |
| #390 | SELECT fact_id, subject, predicate, object, confidence, timestamp |
| #391 | FROM facts |
| #392 | WHERE status = 'active' OR status IS NULL |
| #393 | ORDER BY created_at DESC |
| #394 | LIMIT ? |
| #395 | """, (batch_size,)).fetchall() |
| #396 | |
| #397 | for row in fact_rows: |
| #398 | candidates.append({ |
| #399 | "fact_id": row["fact_id"], |
| #400 | "subject": row["subject"], |
| #401 | "predicate": row["predicate"], |
| #402 | "object": row["object"], |
| #403 | "confidence": row["confidence"], |
| #404 | "timestamp": row["timestamp"], |
| #405 | "source": "fact", |
| #406 | "embedding": _embed(row["object"]), |
| #407 | }) |
| #408 | |
| #409 | # Also pull recent episodic memories |
| #410 | ep_rows = cursor.execute(""" |
| #411 | SELECT id, content, importance, created_at |
| #412 | FROM episodic_memory |
| #413 | ORDER BY created_at DESC |
| #414 | LIMIT ? |
| #415 | """, (batch_size // 2,)).fetchall() |
| #416 | |
| #417 | for row in ep_rows: |
| #418 | content = row["content"] |
| #419 | if content and len(content) > 10: |
| #420 | candidates.append({ |
| #421 | "fact_id": f"ep_{row['id']}", |
| #422 | "subject": "memory", |
| #423 | "predicate": "contains", |
| #424 | "object": content[:300], |
| #425 | "confidence": row["importance"], |
| #426 | "timestamp": row["created_at"], |
| #427 | "source": "episodic", |
| #428 | "embedding": _embed(content[:300]), |
| #429 | }) |
| #430 | |
| #431 | if len(candidates) < SHMR_MIN_CLUSTER_SIZE: |
| #432 | return { |
| #433 | "clusters_found": 0, |
| #434 | "beliefs_generated": 0, |
| #435 | "contradictions_resolved": 0, |
| #436 | "harmony_score_avg": 0.0, |
| #437 | "duration_ms": int((time.perf_counter() - t0) * 1000), |
| #438 | "status": "insufficient_candidates", |
| #439 | } |
| #440 | |
| #441 | # --- Step 2: Cluster by semantic similarity --- |
| #442 | clusters = _cluster_by_similarity(candidates, similarity_threshold) |
| #443 | # Filter small clusters |
| #444 | clusters = [c for c in clusters if len(c) >= SHMR_MIN_CLUSTER_SIZE] |
| #445 | |
| #446 | total_beliefs = 0 |
| #447 | total_contradictions = 0 |
| #448 | harmony_scores = [] |
| #449 | |
| #450 | # --- Step 3: Harmonize each cluster --- |
| #451 | for cluster_idx, cluster in enumerate(clusters): |
| #452 | cluster_id = f"shmr_{int(time.time())}_{cluster_idx}" |
| #453 | |
| #454 | for iteration in range(max_iterations): |
| #455 | context = _format_cluster_for_llm(cluster) |
| #456 | full_prompt = context + "\n\n" + HARMONY_PROMPT |
| #457 | |
| #458 | try: |
| #459 | llm_output = _call_llm(full_prompt) |
| #460 | if not llm_output: |
| #461 | continue |
| #462 | |
| #463 | beliefs = _extract_json_from_llm_output(llm_output) |
| #464 | if not beliefs: |
| #465 | continue |
| #466 | |
| #467 | # Score the result |
| #468 | score = _compute_harmony_score(beliefs, cluster) |
| #469 | harmony_scores.append(score) |
| #470 | |
| #471 | if score >= SHMR_HARMONY_THRESHOLD: |
| #472 | _apply_beliefs(beam.conn, beliefs, cluster, cluster_id) |
| #473 | total_beliefs += len([b for b in beliefs |
| #474 | if b.get("action") in ("create", "update")]) |
| #475 | total_contradictions += len([b for b in beliefs |
| #476 | if b.get("action") == "dampen"]) |
| #477 | break # Converged for this cluster |
| #478 | else: |
| #479 | # Repopulate cluster with beliefs for next iteration |
| #480 | for b in beliefs: |
| #481 | cluster.append({ |
| #482 | "subject": b.get("subject", ""), |
| #483 | "predicate": b.get("predicate", ""), |
| #484 | "object": b.get("object", ""), |
| #485 | "confidence": b.get("confidence", 0.5), |
| #486 | "source": "belief_candidate", |
| #487 | "embedding": _embed(b.get("object", "")), |
| #488 | }) |
| #489 | |
| #490 | except Exception as e: |
| #491 | logger.warning(f"SHMR cluster {cluster_id} iteration {iteration}: {e}") |
| #492 | continue |
| #493 | |
| #494 | duration_ms = int((time.perf_counter() - t0) * 1000) |
| #495 | avg_score = float(np.mean(harmony_scores)) if harmony_scores else 0.0 |
| #496 | |
| #497 | # --- Step 4: Log the resonance --- |
| #498 | try: |
| #499 | cursor.execute(""" |
| #500 | INSERT INTO memory_resonance_log |
| #501 | (session_id, cluster_count, beliefs_generated, |
| #502 | contradictions_resolved, harmony_score_avg, duration_ms) |
| #503 | VALUES (?, ?, ?, ?, ?, ?) |
| #504 | """, ( |
| #505 | beam.session_id, |
| #506 | len(clusters), |
| #507 | total_beliefs, |
| #508 | total_contradictions, |
| #509 | round(avg_score, 4), |
| #510 | duration_ms, |
| #511 | )) |
| #512 | beam.conn.commit() |
| #513 | except Exception: |
| #514 | pass |
| #515 | |
| #516 | return { |
| #517 | "clusters_found": len(clusters), |
| #518 | "beliefs_generated": total_beliefs, |
| #519 | "contradictions_resolved": total_contradictions, |
| #520 | "harmony_score_avg": round(avg_score, 4), |
| #521 | "duration_ms": duration_ms, |
| #522 | "status": "harmonized" if total_beliefs > 0 else "no_convergence", |
| #523 | } |
| #524 | |
| #525 | |
| #526 | def recall_beliefs(beam, query: str, top_k: int = 10) -> List[Dict]: |
| #527 | """Search harmonic beliefs for a given query. |
| #528 | |
| #529 | Used by recall() when harmonic=True flag is set. |
| #530 | """ |
| #531 | cursor = beam.conn.cursor() |
| #532 | _init_schema(beam.conn) |
| #533 | |
| #534 | try: |
| #535 | query_emb = _embed(query) |
| #536 | query_blob = query_emb.tobytes() |
| #537 | |
| #538 | # Search by embedding on object text |
| #539 | results = [] |
| #540 | rows = cursor.execute(""" |
| #541 | SELECT belief_id, subject, predicate, object, confidence, |
| #542 | provenance, created_at |
| #543 | FROM harmonic_beliefs |
| #544 | ORDER BY confidence DESC |
| #545 | LIMIT ? |
| #546 | """, (top_k * 2,)).fetchall() |
| #547 | |
| #548 | # Score by embedding similarity |
| #549 | scored = [] |
| #550 | for row in rows: |
| #551 | try: |
| #552 | belief_emb = _embed(row["object"]) |
| #553 | sim = _cosine_similarity(query_emb, belief_emb) |
| #554 | scored.append((sim * row["confidence"], row)) |
| #555 | except Exception: |
| #556 | scored.append((row["confidence"] * 0.3, row)) |
| #557 | |
| #558 | scored.sort(key=lambda x: x[0], reverse=True) |
| #559 | |
| #560 | for score, row in scored[:top_k]: |
| #561 | results.append({ |
| #562 | "content": row["object"], |
| #563 | "score": round(score, 4), |
| #564 | "belief_id": row["belief_id"], |
| #565 | "subject": row["subject"], |
| #566 | "predicate": row["predicate"], |
| #567 | "provenance": row["provenance"], |
| #568 | "source": "harmonic_belief", |
| #569 | }) |
| #570 | |
| #571 | return results |
| #572 | except Exception: |
| #573 | return [] |
| #574 | |
| #575 | |
| #576 | # ============================================================ |
| #577 | # Phase 3A: Reflective Recall (single-pass fact synthesis) |
| #578 | # ============================================================ |
| #579 | |
| #580 | REFLECTION_PROMPT = """You are a memory reasoning assistant. You have retrieved facts |
| #581 | from a conversation database and need to synthesize a coherent answer. |
| #582 | |
| #583 | QUESTION: {question} |
| #584 | |
| #585 | RETRIEVED FACTS: |
| #586 | {fact_context} |
| #587 | |
| #588 | Based on these facts, provide a concise synthesis (2-4 sentences) that: |
| #589 | 1. Answers the question directly if the facts are sufficient |
| #590 | 2. Identifies any contradictions or gaps in the facts |
| #591 | 3. Notes temporal context (dates, order of events) if present |
| #592 | 4. If facts are insufficient, states what's missing clearly |
| #593 | |
| #594 | SYNTHESIS:""" |
| #595 | |
| #596 | |
| #597 | def reflect(beam, question: str, facts: List[Dict] = None, |
| #598 | top_k: int = 10) -> Optional[str]: |
| #599 | """Single-pass reflective synthesis over retrieved facts. |
| #600 | |
| #601 | Takes a question and a list of fact dicts (from fact_recall()), sends them |
| #602 | to an LLM, and returns a coherent synthesis paragraph. This synthesis is |
| #603 | then injected as additional context for the final answering LLM. |
| #604 | |
| #605 | This is Phase 3A: lightweight, works with any LLM, no iteration needed. |
| #606 | Phase 3B (SHMR harmonize()) replaces this with multi-iteration harmony loop. |
| #607 | |
| #608 | Args: |
| #609 | beam: BeamMemory instance (for fact_recall if facts not provided) |
| #610 | question: The question to synthesize for |
| #611 | facts: Pre-retrieved facts (if None, calls fact_recall automatically) |
| #612 | top_k: Max facts to include in the reflection |
| #613 | |
| #614 | Returns: |
| #615 | Synthesis string, or None if no facts available. |
| #616 | """ |
| #617 | # Get facts if not provided |
| #618 | if facts is None and beam is not None: |
| #619 | try: |
| #620 | facts = beam.fact_recall(question, top_k=top_k) |
| #621 | except Exception: |
| #622 | return None |
| #623 | |
| #624 | if not facts: |
| #625 | return None |
| #626 | |
| #627 | # Build fact context (limit to top_k, sort by score) |
| #628 | sorted_facts = sorted(facts, key=lambda f: f.get("score", 0), reverse=True)[:top_k] |
| #629 | fact_lines = [] |
| #630 | for i, f in enumerate(sorted_facts): |
| #631 | content = f.get("content", "") |
| #632 | score = f.get("score", 0.5) |
| #633 | source = f.get("source", "fact") |
| #634 | fact_lines.append(f"[{i}] ({source}, conf={score:.2f}) {content}") |
| #635 | |
| #636 | fact_context = "\n".join(fact_lines) |
| #637 | prompt = REFLECTION_PROMPT.format(question=question, fact_context=fact_context) |
| #638 | |
| #639 | synthesis = _call_llm(prompt) |
| #640 | if synthesis and len(synthesis.strip()) > 10: |
| #641 | return synthesis.strip() |
| #642 | return None |
| #643 | |
| #644 | |
| #645 | def get_resonance_log(beam, limit: int = 10) -> List[Dict]: |
| #646 | """Get recent harmonization run logs.""" |
| #647 | cursor = beam.conn.cursor() |
| #648 | _init_schema(beam.conn) |
| #649 | try: |
| #650 | rows = cursor.execute(""" |
| #651 | SELECT * FROM memory_resonance_log |
| #652 | ORDER BY created_at DESC LIMIT ? |
| #653 | """, (limit,)).fetchall() |
| #654 | return [dict(r) for r in rows] |
| #655 | except Exception: |
| #656 | return [] |
| #657 |