# app/core/note_payload.py # Version: 1.2.0 (2025-11-08) # Purpose: # Build robust Qdrant payloads for NOTE points. # # Highlights: # - Works with both dict-like inputs and ParsedNote-like objects (attribute access). # - Accepts legacy/extra kwargs (e.g., vault_root) without failing. # - Copies canonical fields: id/note_id, title, type, tags, path, text (if present). # - Reliably propagates `retriever_weight` into the payload if set in frontmatter # (frontmatter.retriever_weight or frontmatter.retriever.weight) or provided explicitly. # # Backward compatibility: # - Signature accepts **kwargs (e.g., vault_root) because some callers pass it. # - Both 'id' and 'note_id' are written for compatibility with existing queries. # # Usage: # payload = make_note_payload(parsed_note, retriever_weight=None, vault_root="/path/to/vault") # # Changelog: # 1.2.0 (2025-11-08) Accept legacy kwargs, robust getters, propagate retriever_weight. # 1.1.0 (2025-11-08) Initial robust rewrite with attribute/dict support. from __future__ import annotations from pathlib import Path from typing import Any, Dict, List, Optional def _get(obj: Any, key: str, default: Any = None) -> Any: """Robust getter: attribute first, then dict.""" if obj is None: return default if hasattr(obj, key): try: val = getattr(obj, key) return default if val is None else val except Exception: pass if isinstance(obj, dict): if key in obj: val = obj.get(key, default) return default if val is None else val return default def _get_frontmatter(note: Any) -> Dict[str, Any]: fm = _get(note, "frontmatter", None) if isinstance(fm, dict): return fm meta = _get(note, "meta", None) if isinstance(meta, dict) and isinstance(meta.get("frontmatter"), dict): return meta["frontmatter"] return {} def _get_from_frontmatter(fm: Dict[str, Any], key: str, default: Any = None) -> Any: if not isinstance(fm, dict): return default if key in fm: val = fm.get(key, default) return default if val is None else val return default def _coerce_tags(val: Any) -> List[str]: if val is None: return [] if isinstance(val, list): return [str(x) for x in val] if isinstance(val, str): parts = [t.strip() for t in val.split(",")] return [p for p in parts if p] return [] def _resolve_retriever_weight(fm: Dict[str, Any], explicit: Optional[float]) -> Optional[float]: # 1) explicit argument wins if explicit is not None: try: return float(explicit) except Exception: return None # 2) frontmatter.retriever_weight val = _get_from_frontmatter(fm, "retriever_weight", None) if isinstance(val, (int, float)): return float(val) # 3) frontmatter.retriever.weight retr = fm.get("retriever") if isinstance(retr, dict): v = retr.get("weight") if isinstance(v, (int, float)): return float(v) return None def _resolve_path(note: Any, fm: Dict[str, Any], vault_root: Optional[str]) -> Optional[str]: """Try to determine a stable relative path for diagnostics/traceability.""" path = _get_from_frontmatter(fm, "path", None) if path is None: path = _get(note, "path", None) or _get(note, "source", None) or _get(note, "filepath", None) if path is None: return None try: if vault_root: vr = Path(vault_root) # Avoid Windows drive quirks: use Pure/Path consistently rel = Path(path) try: path_rel = str(rel.relative_to(vr)) except Exception: # If 'path' is absolute not under vault_root, just return as-is path_rel = str(rel) return path_rel except Exception: pass return str(path) def make_note_payload( note: Any, *, retriever_weight: Optional[float] = None, vault_root: Optional[str] = None, **kwargs, ) -> Dict[str, Any]: """ Build a Qdrant payload dict for a NOTE. Parameters ---------- note : Any Parsed note (dict or object with attributes). retriever_weight : Optional[float] Optional override; if None, value is read from frontmatter. vault_root : Optional[str] Optional base path to compute relative 'path' if possible. **kwargs : Ignored extra options to remain compatible with callers. Returns ------- Dict[str, Any] Payload ready for Qdrant upsert. """ fm = _get_frontmatter(note) # id / note_id note_id = _get_from_frontmatter(fm, "id", None) or _get(note, "note_id", None) or _get(note, "id", None) title = _get_from_frontmatter(fm, "title", None) or _get(note, "title", None) ntype = _get_from_frontmatter(fm, "type", None) or _get(note, "type", None) tags = _coerce_tags(_get_from_frontmatter(fm, "tags", None) or _get(note, "tags", None)) # Optional text for notes collection (only if present; we don't force it) text = _get(note, "text", None) if text is None and isinstance(note, dict): text = note.get("body") or note.get("content") # Path resolution path = _resolve_path(note, fm, vault_root) payload: Dict[str, Any] = {} if note_id is not None: payload["id"] = note_id # keep for legacy queries payload["note_id"] = note_id # canonical if title is not None: payload["title"] = title if ntype is not None: payload["type"] = ntype if tags: payload["tags"] = tags if path is not None: payload["path"] = path if text is not None: payload["text"] = text rw = _resolve_retriever_weight(fm, retriever_weight) if rw is not None: payload["retriever_weight"] = rw return payload