mindnet/app/core/note_payload.py

140 lines
3.9 KiB
Python
Raw Normal View History

2025-11-08 21:31:24 +01:00
"""
note_payload.py — mindnet core payload builders
Version: 1.3.1 (2025-11-08)
Purpose
-------
Build a robust, forward-compatible note payload for Qdrant upserts.
This module is intentionally defensive:
- Accepts both dict-like "parsed note" objects and dataclass/objects with attributes.
- Tolerates extra kwargs from different callers (e.g., `vault_root`, `prefix`, etc.).
- Ensures `retriever_weight` is resolved and present in the payload if available.
Contract
--------
make_note_payload(note, **kwargs) -> Dict[str, Any]
Expected minimal fields in returned payload:
- note_id (str)
- title (str)
- type (str)
- path (str or None) # relative to vault_root when provided
- tags (List[str])
- retriever_weight (float or None) # if available
"""
2025-09-09 16:31:25 +02:00
from __future__ import annotations
2025-11-08 21:26:42 +01:00
from pathlib import Path
2025-11-08 21:31:24 +01:00
from typing import Any, Dict, Iterable, List, Mapping, Optional, Union
2025-11-08 21:21:57 +01:00
def _get(obj: Any, key: str, default: Any = None) -> Any:
2025-11-08 21:31:24 +01:00
"""Try to read `key` from mapping or attribute; else default."""
2025-11-08 21:21:57 +01:00
if obj is None:
return default
2025-11-08 21:31:24 +01:00
if isinstance(obj, Mapping):
return obj.get(key, default)
# attribute access
return getattr(obj, key, default)
2025-11-08 21:21:57 +01:00
2025-11-08 21:31:24 +01:00
def _get_frontmatter(note: Any) -> Mapping[str, Any]:
fm = _get(note, "frontmatter", {})
if isinstance(fm, Mapping):
2025-11-08 21:21:57 +01:00
return fm
2025-11-08 21:31:24 +01:00
return {} # be safe
2025-11-08 21:19:18 +01:00
2025-11-08 21:21:57 +01:00
2025-11-08 21:31:24 +01:00
def _resolve_retriever_weight(explicit: Any, fm: Mapping[str, Any]) -> Optional[float]:
"""
Priority:
1) explicit kwarg retriever_weight
2) frontmatter['retriever_weight']
3) frontmatter['retriever']['weight']
"""
def to_float(v: Any) -> Optional[float]:
2025-11-08 21:26:42 +01:00
try:
2025-11-08 21:31:24 +01:00
if v is None:
return None
return float(v)
2025-11-08 21:26:42 +01:00
except Exception:
return None
2025-11-08 21:31:24 +01:00
if explicit is not None:
return to_float(explicit)
if "retriever_weight" in fm:
return to_float(fm.get("retriever_weight"))
2025-11-08 21:21:57 +01:00
retr = fm.get("retriever")
2025-11-08 21:31:24 +01:00
if isinstance(retr, Mapping) and "weight" in retr:
return to_float(retr.get("weight"))
2025-11-08 21:19:18 +01:00
return None
2025-09-09 16:31:25 +02:00
2025-11-08 21:31:24 +01:00
def _to_rel_path(abs_path: Optional[Union[str, Path]], vault_root: Optional[Union[str, Path]]) -> Optional[str]:
if abs_path is None:
2025-11-08 21:26:42 +01:00
return None
try:
2025-11-08 21:31:24 +01:00
p = Path(abs_path)
2025-11-08 21:26:42 +01:00
if vault_root:
try:
2025-11-08 21:31:24 +01:00
rp = p.relative_to(Path(vault_root))
return str(rp)
2025-11-08 21:26:42 +01:00
except Exception:
2025-11-08 21:31:24 +01:00
return str(p)
return str(p)
2025-11-08 21:26:42 +01:00
except Exception:
2025-11-08 21:31:24 +01:00
return str(abs_path)
2025-11-08 21:26:42 +01:00
2025-09-09 16:31:25 +02:00
def make_note_payload(
2025-11-08 21:21:57 +01:00
note: Any,
2025-11-08 21:31:24 +01:00
*args, # tolerate older/other callers
**kwargs: Any,
2025-09-09 16:31:25 +02:00
) -> Dict[str, Any]:
"""
2025-11-08 21:31:24 +01:00
Build a normalized note payload for Qdrant.
Unknown kwargs are ignored to keep the function forward-compatible.
Recognized kwargs:
- vault_root: base path to make `path` relative (optional)
- retriever_weight: explicit override (optional)
2025-09-09 16:31:25 +02:00
"""
2025-11-08 21:31:24 +01:00
vault_root = kwargs.get("vault_root")
explicit_weight = kwargs.get("retriever_weight")
2025-11-08 21:21:57 +01:00
fm = _get_frontmatter(note)
2025-11-08 21:31:24 +01:00
note_id = _get(note, "note_id") or _get(note, "id")
if not note_id:
# Try from frontmatter
note_id = fm.get("id")
title = _get(note, "title") or fm.get("title")
ntype = _get(note, "type") or fm.get("type")
tags = _get(note, "tags") or fm.get("tags") or []
if not isinstance(tags, list):
tags = list(tags) if tags else []
path_val = _get(note, "path") or _get(note, "abs_path") or fm.get("path")
payload: Dict[str, Any] = {
"note_id": note_id,
"title": title,
"type": ntype,
"tags": tags,
"path": _to_rel_path(path_val, vault_root),
"retriever_weight": _resolve_retriever_weight(explicit_weight, fm),
}
# Also surface explicit frontmatter fields (non-conflicting) if present
for k in ("status", "created", "updated"):
v = fm.get(k)
if v is not None and k not in payload:
payload[k] = v
2025-09-09 16:55:36 +02:00
return payload