feat: works, but no title gen in opencode

This commit is contained in:
Apunkt 2026-05-27 18:50:08 +02:00
parent 37ee97b8ec
commit 5da28062b9
No known key found for this signature in database

View file

@ -651,6 +651,64 @@ class QdrantStore:
offset = next_offset offset = next_offset
return [self._from_point(p) for p in all_points] return [self._from_point(p) for p in all_points]
@staticmethod
def _parse_where(where: str | None) -> Filter | None:
"""Parse a LanceDB-style where clause into a Qdrant Filter.
Supported grammar:
- ``key = 'value'`` or ``key = "value"`` MatchValue
- ``key > N`` or ``key >= N`` Range(gt/gte)
- ``key < N`` or ``key <= N`` Range(lt/lte)
- Multiple clauses joined by AND Filter(must=[...])
Numeric values are auto-detected; string values must be
quoted. Returns None if the clause is empty or cannot be parsed.
"""
if not where:
return None
clauses = re.split(r'\s+AND\s+', where, flags=re.IGNORECASE)
conditions: list = []
for clause in clauses:
clause = clause.strip()
if not clause:
continue
m = re.match(
r"""([a-z_]+)\s*(>=|<=|>|<)\s*(-?\d+(?:\.\d+)?)\s*$""",
clause,
)
if m:
key, op, val_str = m.group(1), m.group(2), m.group(3)
val = float(val_str)
if op == ">":
conditions.append(FieldCondition(key=key, range=models.Range(gt=val)))
elif op == ">=":
conditions.append(FieldCondition(key=key, range=models.Range(gte=val)))
elif op == "<":
conditions.append(FieldCondition(key=key, range=models.Range(lt=val)))
elif op == "<=":
conditions.append(FieldCondition(key=key, range=models.Range(lte=val)))
continue
m = re.match(
r"""([a-z_]+)\s*=\s*['"]([^'"]+)['"]\s*$""",
clause,
)
if m:
key, val = m.group(1), m.group(2)
conditions.append(
FieldCondition(key=key, match=MatchValue(value=val))
)
continue
continue
if not conditions:
return None
return Filter(must=conditions)
def iter_records( def iter_records(
self, self,
*, *,
@ -661,11 +719,10 @@ class QdrantStore:
"""Streaming iterator over records (filtered by table=records).""" """Streaming iterator over records (filtered by table=records)."""
offset = None offset = None
while True: while True:
# Build filter: always include table=records, optionally add tier
conditions = [FieldCondition(key="table", match=MatchValue(value=RECORDS_TABLE))] conditions = [FieldCondition(key="table", match=MatchValue(value=RECORDS_TABLE))]
if where and where.startswith("tier = "): where_filter = self._parse_where(where)
tier = where.split("'")[1] if where_filter is not None:
conditions.append(FieldCondition(key="tier", match=MatchValue(value=tier))) conditions.extend(where_filter.must)
qdrant_filter = Filter(must=conditions) if conditions else None qdrant_filter = Filter(must=conditions) if conditions else None
points, next_offset = self._client.scroll( points, next_offset = self._client.scroll(
@ -698,9 +755,9 @@ class QdrantStore:
offset = None offset = None
while True: while True:
conditions = [FieldCondition(key="table", match=MatchValue(value=RECORDS_TABLE))] conditions = [FieldCondition(key="table", match=MatchValue(value=RECORDS_TABLE))]
if where and where.startswith("tier = "): where_filter = self._parse_where(where)
tier = where.split("'")[1] if where_filter is not None:
conditions.append(FieldCondition(key="tier", match=MatchValue(value=tier))) conditions.extend(where_filter.must)
qdrant_filter = Filter(must=conditions) if conditions else None qdrant_filter = Filter(must=conditions) if conditions else None
points, next_offset = self._client.scroll( points, next_offset = self._client.scroll(
@ -712,7 +769,6 @@ class QdrantStore:
with_vectors=False, with_vectors=False,
) )
for point in points: for point in points:
# Filter to requested columns
row = {k: v for k, v in point.payload.items() if k in columns} row = {k: v for k, v in point.payload.items() if k in columns}
if "id" in columns: if "id" in columns:
row["id"] = point.id row["id"] = point.id
@ -1349,7 +1405,14 @@ class QdrantStore:
return pd.DataFrame(columns=["src", "dst", "edge_type", "weight", "updated_at"]) return pd.DataFrame(columns=["src", "dst", "edge_type", "weight", "updated_at"])
def records_as_dataframe(self) -> "pd.DataFrame": def records_as_dataframe(self) -> "pd.DataFrame":
"""Return all records from the records collection as a pandas DataFrame.""" """Return all records from the records collection as a pandas DataFrame.
Column types match the LanceDB schema:
- timestamps are datetime objects (not str)
- structure_hv is bytes (not hex string)
- provenance_json is AES-256-GCM ciphertext (not plaintext JSON)
- tags_json is a JSON string
"""
try: try:
records = self.all_records() records = self.all_records()
if not records: if not records:
@ -1360,10 +1423,13 @@ class QdrantStore:
"stability", "difficulty", "last_reviewed", "stability", "difficulty", "last_reviewed",
"never_decay", "never_merge", "detail_level", "never_decay", "never_merge", "detail_level",
"s5_trust_score", "structure_hv", "s5_trust_score", "structure_hv",
"provenance_json", "created_at", "schema_version", "provenance_json", "created_at", "updated_at", "schema_version",
]) ])
rows = [] rows = []
for r in records: for r in records:
# Re-encrypt provenance to match LanceDB's ciphertext column.
prov_plain = json.dumps(r.provenance or [])
prov_ct = self._encrypt_for_record(r.id, prov_plain)
rows.append({ rows.append({
"id": str(r.id), "id": str(r.id),
"tier": r.tier, "tier": r.tier,
@ -1372,19 +1438,20 @@ class QdrantStore:
"community_id": str(r.community_id) if r.community_id else None, "community_id": str(r.community_id) if r.community_id else None,
"centrality": r.centrality, "centrality": r.centrality,
"pinned": r.pinned, "pinned": r.pinned,
"tags_json": r.tags_json if hasattr(r, "tags_json") else "[]", "tags_json": json.dumps(r.tags),
"language": r.language, "language": r.language,
"aaak_index": r.aaak_index, "aaak_index": r.aaak_index,
"stability": r.stability, "stability": r.stability,
"difficulty": r.difficulty, "difficulty": r.difficulty,
"last_reviewed": str(r.last_reviewed) if r.last_reviewed else None, "last_reviewed": r.last_reviewed,
"never_decay": r.never_decay, "never_decay": r.never_decay,
"never_merge": r.never_merge, "never_merge": r.never_merge,
"detail_level": r.detail_level, "detail_level": r.detail_level,
"s5_trust_score": r.s5_trust_score, "s5_trust_score": r.s5_trust_score,
"structure_hv": r.structure_hv.hex() if r.structure_hv else "", "structure_hv": bytes(r.structure_hv) if r.structure_hv else b"",
"provenance_json": json.dumps(r.provenance) if r.provenance else "[]", "provenance_json": prov_ct,
"created_at": str(r.created_at) if r.created_at else None, "created_at": r.created_at,
"updated_at": r.updated_at,
"schema_version": r.schema_version, "schema_version": r.schema_version,
}) })
return pd.DataFrame(rows) return pd.DataFrame(rows)
@ -1396,7 +1463,7 @@ class QdrantStore:
"stability", "difficulty", "last_reviewed", "stability", "difficulty", "last_reviewed",
"never_decay", "never_merge", "detail_level", "never_decay", "never_merge", "detail_level",
"s5_trust_score", "structure_hv", "s5_trust_score", "structure_hv",
"provenance_json", "created_at", "schema_version", "provenance_json", "created_at", "updated_at", "schema_version",
]) ])
# ------------------------------------------------------------------ db shim # ------------------------------------------------------------------ db shim
@ -1674,6 +1741,15 @@ class QdrantStore:
if isinstance(ts_val, datetime) if isinstance(ts_val, datetime)
else str(ts_val) if ts_val else "" else str(ts_val) if ts_val else ""
) )
# Compute ts_epoch for numeric range filtering (matches events_add)
ts_epoch = None
if isinstance(ts_val, datetime):
ts_epoch = ts_val.timestamp()
elif ts_str:
try:
ts_epoch = datetime.fromisoformat(ts_str).timestamp()
except (ValueError, TypeError):
pass
points.append(PointStruct( points.append(PointStruct(
id=str(row.get("id", uuid4())), id=str(row.get("id", uuid4())),
vector={}, vector={},
@ -1685,6 +1761,7 @@ class QdrantStore:
"severity": row.get("severity") or "", "severity": row.get("severity") or "",
"domain": row.get("domain") or "", "domain": row.get("domain") or "",
"ts": ts_str, "ts": ts_str,
"ts_epoch": ts_epoch,
"data_json": row.get("data_json", ""), "data_json": row.get("data_json", ""),
"session_id": row.get("session_id", "-"), "session_id": row.get("session_id", "-"),
"source_ids_json": row.get("source_ids_json", "[]"), "source_ids_json": row.get("source_ids_json", "[]"),
@ -1747,105 +1824,70 @@ class QdrantStore:
) )
def _add_records(self, rows: list[dict]) -> None: def _add_records(self, rows: list[dict]) -> None:
"""Insert record rows — converts dicts to PointStruct via _to_point.""" """Insert record rows — builds PointStruct directly from row dicts.
The row dicts come from _to_row() which already contains
encrypted ciphertext for literal_surface / provenance_json /
profile_modulation_gain_json. Building a MemoryRecord and
passing it through _to_point would double-encrypt those fields,
so we construct the PointStruct payload directly here.
"""
points = [] points = []
for row in rows: for row in rows:
# Build a minimal MemoryRecord from the dict so _to_point works def _ts(val):
from uuid import UUID as _UUID if val is None:
rec = MemoryRecord( return None
id=_UUID(row["id"]), if isinstance(val, datetime):
tier=row.get("tier", "episodic"), return val.isoformat()
literal_surface="", return str(val)
aaak_index=row.get("aaak_index", ""),
embedding=list(row.get("embedding", [0.0] * self._store._embed_dim)), # Decode structure_hv from bytes (LanceDB pa.binary → bytes)
community_id=_UUID(row["community_id"]) if row.get("community_id") else None, structure_raw = row.get("structure_hv")
centrality=float(row.get("centrality", 0.0)), if isinstance(structure_raw, (bytes, bytearray)):
detail_level=int(row.get("detail_level", 1)), structure_b64 = base64.b64encode(structure_raw).decode("ascii")
pinned=bool(row.get("pinned", False)), elif isinstance(structure_raw, str):
stability=float(row.get("stability", 0.0)), # Already base64 or hex — keep as-is
difficulty=float(row.get("difficulty", 0.0)), structure_b64 = structure_raw
last_reviewed=None, else:
never_decay=bool(row.get("never_decay", False)), structure_b64 = ""
never_merge=bool(row.get("never_merge", False)),
provenance=[], points.append(PointStruct(
created_at=None, id=str(row.get("id", uuid4())),
updated_at=None, vector=list(row.get("embedding", [0.0] * self._store._embed_dim)),
tags=json.loads(row.get("tags_json") or "[]"), payload={
language=row.get("language", "en"), "table": RECORDS_TABLE,
s5_trust_score=float(row.get("s5_trust_score", 0.5)), "group_id": self._store._group_id,
profile_modulation_gain={}, "tier": row.get("tier", "episodic"),
schema_version=int(row.get("schema_version", 1)), "literal_surface": row.get("literal_surface", ""),
structure_hv=b"", "aaak_index": row.get("aaak_index", ""),
) "structure_hv": structure_b64,
points.append(self._store._to_point(rec)) "community_id": row.get("community_id", ""),
"centrality": float(row.get("centrality", 0.0)),
"detail_level": int(row.get("detail_level", 1)),
"pinned": bool(row.get("pinned", False)),
"stability": float(row.get("stability", 0.0)),
"difficulty": float(row.get("difficulty", 0.0)),
"last_reviewed": _ts(row.get("last_reviewed")),
"never_decay": bool(row.get("never_decay", False)),
"never_merge": bool(row.get("never_merge", False)),
"provenance_json": row.get("provenance_json", "[]"),
"created_at": _ts(row.get("created_at")),
"updated_at": _ts(row.get("updated_at")),
"tags_json": row.get("tags_json", "[]"),
"language": str(row.get("language", "en")),
"s5_trust_score": float(row.get("s5_trust_score", 0.5)),
"profile_modulation_gain_json": row.get("profile_modulation_gain_json", "{}"),
"schema_version": int(row.get("schema_version", 1)),
},
))
if points: if points:
self._store._client.upsert( self._store._client.upsert(
collection_name=RECORDS_TABLE, points=points, collection_name=RECORDS_TABLE, points=points,
) )
def _parse_where(self, where: str | None) -> Filter | None: def _parse_where(self, where: str | None) -> Filter | None:
"""Parse a LanceDB-style where clause into a Qdrant Filter. """Delegate to QdrantStore._parse_where (single source of truth)."""
return QdrantStore._parse_where(where)
Supported grammar:
- ``key = 'value'`` or ``key = "value"`` MatchValue
- ``key > N`` or ``key >= N`` Range(gt/gte)
- ``key < N`` or ``key <= N`` Range(lt/lte)
- Multiple clauses joined by AND Filter(must=[...])
Numeric values are auto-detected; string values must be
quoted. Returns None if the clause is empty or cannot be parsed.
"""
if not where:
return None
# Split on AND (case-insensitive) to get individual clauses.
# Use regex to avoid splitting inside quoted strings.
clauses = re.split(r'\s+AND\s+', where, flags=re.IGNORECASE)
conditions: list = []
for clause in clauses:
clause = clause.strip()
if not clause:
continue
# Try numeric comparison operators first: >, >=, <, <=
m = re.match(
r"""([a-z_]+)\s*(>=|<=|>|<)\s*(-?\d+(?:\.\d+)?)\s*$""",
clause,
)
if m:
key, op, val_str = m.group(1), m.group(2), m.group(3)
val = float(val_str)
if op == ">":
conditions.append(FieldCondition(key=key, range=models.Range(gt=val)))
elif op == ">=":
conditions.append(FieldCondition(key=key, range=models.Range(gte=val)))
elif op == "<":
conditions.append(FieldCondition(key=key, range=models.Range(lt=val)))
elif op == "<=":
conditions.append(FieldCondition(key=key, range=models.Range(lte=val)))
continue
# Try string equality: key = 'value' or key = "value"
m = re.match(
r"""([a-z_]+)\s*=\s*['"]([^'"]+)['"]\s*$""",
clause,
)
if m:
key, val = m.group(1), m.group(2)
conditions.append(
FieldCondition(key=key, match=MatchValue(value=val))
)
continue
# Unparseable clause — skip silently (LanceDB would too).
continue
if not conditions:
return None
if len(conditions) == 1:
return Filter(must=conditions)
return Filter(must=conditions)
@property @property
def db(self) -> "QdrantStore._DbShim": def db(self) -> "QdrantStore._DbShim":