mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(rag): decode jsonb metadata returned as a string by asyncpg
VectorStore.search / list_docs called dict(row["metadata"]), but asyncpg returns jsonb as a JSON *string* (no codec on the pool), so dict() iterated characters and raised 'dictionary update sequence element #0 has length 1; 2 is required' — making every KB search fail at the row-mapping step once migration 030 let the query reach rows (it was masked before by the missing content column). Add _as_dict(): json.loads a string, pass dicts through, null/non-object -> {}. Caught by live end-to-end verification on the NAS.
This commit is contained in:
@@ -53,6 +53,22 @@ def _vec_to_str(embedding: list[float]) -> str:
|
|||||||
return "[" + ",".join(str(x) for x in embedding) + "]"
|
return "[" + ",".join(str(x) for x in embedding) + "]"
|
||||||
|
|
||||||
|
|
||||||
|
def _as_dict(value: Any) -> dict[str, Any]:
|
||||||
|
"""Decode a ``jsonb`` column value into a dict.
|
||||||
|
|
||||||
|
No json codec is registered on the pool, so asyncpg returns ``jsonb`` as a
|
||||||
|
JSON *string*; ``dict(value)`` would then iterate characters and raise
|
||||||
|
"dictionary update sequence element #0 has length 1; 2 is required". Handle
|
||||||
|
both the string form and an already-decoded mapping (and null → ``{}``).
|
||||||
|
"""
|
||||||
|
if not value:
|
||||||
|
return {}
|
||||||
|
if isinstance(value, str):
|
||||||
|
loaded = json.loads(value)
|
||||||
|
return dict(loaded) if isinstance(loaded, dict) else {}
|
||||||
|
return dict(value)
|
||||||
|
|
||||||
|
|
||||||
class VectorStore:
|
class VectorStore:
|
||||||
"""Async vector store backed by PostgreSQL + pgvector.
|
"""Async vector store backed by PostgreSQL + pgvector.
|
||||||
|
|
||||||
@@ -259,7 +275,7 @@ class VectorStore:
|
|||||||
chunk=row["content"],
|
chunk=row["content"],
|
||||||
source=row["source"],
|
source=row["source"],
|
||||||
score=float(row["score"]),
|
score=float(row["score"]),
|
||||||
metadata=dict(row["metadata"]) if row["metadata"] else {},
|
metadata=_as_dict(row["metadata"]),
|
||||||
)
|
)
|
||||||
for row in rows
|
for row in rows
|
||||||
]
|
]
|
||||||
@@ -307,7 +323,7 @@ class VectorStore:
|
|||||||
"indexed_at": row["indexed_at"].isoformat()
|
"indexed_at": row["indexed_at"].isoformat()
|
||||||
if row["indexed_at"]
|
if row["indexed_at"]
|
||||||
else "",
|
else "",
|
||||||
"metadata": dict(row["metadata"]) if row["metadata"] else {},
|
"metadata": _as_dict(row["metadata"]),
|
||||||
}
|
}
|
||||||
for row in rows
|
for row in rows
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""VectorStore must decode jsonb metadata returned by asyncpg as a string.
|
||||||
|
|
||||||
|
asyncpg returns ``jsonb`` columns as a JSON *string* (no codec on the pool), so
|
||||||
|
``dict(value)`` iterated characters and raised "dictionary update sequence
|
||||||
|
element #0 has length 1; 2 is required" — which made every KB search fail at the
|
||||||
|
row-mapping step once the schema (migration 030) let the query reach rows.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from roboco.services.optimal_brain.vector_store import _as_dict
|
||||||
|
|
||||||
|
|
||||||
|
def test_decodes_jsonb_string() -> None:
|
||||||
|
# The bug case: asyncpg hands back a JSON string, not a dict.
|
||||||
|
assert _as_dict('{"agent": "be-dev-1", "type": "journal"}') == {
|
||||||
|
"agent": "be-dev-1",
|
||||||
|
"type": "journal",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_passes_through_mapping() -> None:
|
||||||
|
assert _as_dict({"k": "v"}) == {"k": "v"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_none_and_empty_become_empty_dict() -> None:
|
||||||
|
assert _as_dict(None) == {}
|
||||||
|
assert _as_dict("") == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_object_json_becomes_empty_dict() -> None:
|
||||||
|
# A bare JSON scalar/array is not a metadata mapping.
|
||||||
|
assert _as_dict("[1, 2, 3]") == {}
|
||||||
|
assert _as_dict('"just a string"') == {}
|
||||||
Reference in New Issue
Block a user