bytedance/deer-flow · error · ValueError

retrieval fact.content must be a non-empty string

Error message

retrieval fact.content must be a non-empty string

What it means

ValueError from FTS5RetrievalAdapter._document: a fact's 'content' is missing, not a string, empty, or whitespace-only. Content is what FTS5 indexes, so a contentless fact is unindexable and rejected rather than stored as a dead row.

Solutions

  1. Skip facts with blank content before enqueueing for indexing (they carry nothing searchable).
  2. If content is structured, serialize it to a meaningful text representation first.
  3. Fix the extractor so it never emits empty-content facts (guard at the source).

Example fix

# before
for fact in facts:
    adapter.upsert(fact, scope)  # raises on blank content

# after
for fact in facts:
    if isinstance(fact.get("content"), str) and fact["content"].strip():
        adapter.upsert(fact, scope)
Defensive patterns

Strategy: validation

Validate before calling

def has_searchable_content(fact) -> bool:
    c = fact.get('content') if isinstance(fact, dict) else None
    return isinstance(c, str) and bool(c.strip())

Type guard

def has_searchable_content(fact: dict) -> bool:
    c = fact.get('content')
    return isinstance(c, str) and c.strip() != ''

Try / catch

try:
    adapter.upsert(fact, scope)
except ValueError as e:
    if 'fact.content' in str(e):
        logger.warning('skipping contentless fact id=%s', fact.get('id'))
        continue
    raise

Prevention

When it happens

Trigger: Indexing a fact dict whose content is None, '', ' ', or a non-str value (e.g. a dict/list payload put under 'content').

Common situations: Fact extraction emitting empty content for structured events; content field holding structured data instead of text; upstream summarization produced an empty string; whitespace-only content from a template render that substituted nothing.

Related errors


AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14). Data as JSON: /api/errors/3d45a473341d1a75. Report an issue: GitHub.

Appendix: source

Thrown at backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/retrieval.py:576

    document id prevents equal fact ids in two user/agent scopes from
    overwriting each other.
    """

    def __init__(self, db_path: str | Path = ":memory:") -> None:
        self._engine = FTS5Retrieval(db_path)

    @staticmethod
    def _document_id(fact_id: str, scope: dict[str, str | None]) -> str:
        scope_user, scope_agent = _scope_key(scope)
        return json.dumps([scope_user, scope_agent, fact_id], ensure_ascii=False, separators=(",", ":"))

    def _document(self, fact: dict[str, Any], scope: dict[str, str | None]) -> dict[str, Any]:
        fact_id = fact.get("id")
        content = fact.get("content")
        if not isinstance(fact_id, str) or not fact_id:
            raise ValueError("retrieval fact.id must be a non-empty string")
        if not isinstance(content, str) or not content.strip():
            raise ValueError("retrieval fact.content must be a non-empty string")
        scope_user, scope_agent = _scope_key(scope)
        payload = dict(fact)
        payload["scope"] = {"userId": scope.get("userId"), "agentName": scope.get("agentName")}
        source = payload.get("source")
        return {
            "fact_id": self._document_id(fact_id, scope),
            "content": content,
            "category": str(payload.get("category") or "context"),
            "confidence": float(payload.get("confidence") or 0.5),
            "created_at": payload.get("createdAt") if isinstance(payload.get("createdAt"), str) else None,
            "scope_user": scope_user,
            "scope_agent": scope_agent,
            "source": source if isinstance(source, str) else json.dumps(source, ensure_ascii=False, default=str),
            "fact_data": payload,
        }

    def upsert(self, fact: dict[str, Any], *, scope: dict[str, str | None], path: str) -> None:
        del path  # Canonical location belongs to storage; the index is rebuildable.

View on GitHub (pinned to 1dd6ba1acb)