"""SQLite-backed semantic index over memory markdown + daily logs. Each indexable chunk (a non-trivial line from a memory file, or a paragraph from a daily log) is stored with its embedding as a BLOB. Search computes cosine similarity in Python — fine for the scale MohaMind operates at (thousands of chunks, not millions). Features: - `sync()` re-indexes files whose mtime changed since last sync. - `search(query, ...)` returns ranked chunks with source metadata. - Privacy-aware: sensitive categories can be excluded from indexing. - Backend-agnostic: uses any object satisfying the `Embedder` protocol. The file format is boring on purpose — no external vector DB, no compile step, plays well with backup tools. """ from __future__ import annotations import hashlib import math import sqlite3 import struct from dataclasses import dataclass from pathlib import Path from typing import Iterable, Optional from moha_mind.agent.embeddings import Embedder from moha_mind.agent.memory import MEMORY_FILES, MemoryManager from moha_mind.agent.privacy import PrivacyPolicy from moha_mind.utils.logging_config import log @dataclass class SemanticResult: source: str # "memory" | "daily_log" | "note" category: str chunk: str score: float line_number: int = 0 path: str = "" def _pack(vec: list[float]) -> bytes: return struct.pack(f"{len(vec)}f", *vec) def _unpack(blob: bytes) -> list[float]: if not blob: return [] dim = len(blob) // 4 return list(struct.unpack(f"{dim}f", blob)) def _cosine(a: list[float], b: list[float]) -> float: if not a or not b or len(a) != len(b): return 0.0 dot = 0.0 na = 0.0 nb = 0.0 for x, y in zip(a, b): dot += x * y na += x * x nb += y * y if na == 0 or nb == 0: return 0.0 return dot / (math.sqrt(na) * math.sqrt(nb)) def _hash(text: str) -> str: return hashlib.sha1(text.encode("utf-8")).hexdigest()[:16] def _split_chunks(content: str, *, min_len: int = 8) -> list[tuple[int, str]]: """Split a markdown file into (line_number, chunk_text) tuples. Skips headings, comments, and empty lines. Keeps bullets and free prose. """ out: list[tuple[int, str]] = [] for i, raw in enumerate(content.splitlines(), start=1): line = raw.strip() if not line or line.startswith("#") or line.startswith("