All checks were successful
Deploy mindnet to llm-node / deploy (push) Successful in 3s
182 lines
5.8 KiB
Python
182 lines
5.8 KiB
Python
# 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
|